can1357/oh-my-pi · error
OAuth authorization code is invalid or expired. Please try a
Error message
OAuth authorization code is invalid or expired. Please try again.
What it means
The MCP OAuth flow failed and the underlying error contained 'invalid_grant' — the standard OAuth2 token-endpoint error. The authorization code presented during the token exchange is invalid, already used, or expired, so the flow cannot obtain tokens.
Source
Thrown at packages/coding-agent/src/modes/controllers/mcp-command-controller.ts:1036
resource: flow.resource,
};
} catch (error) {
// Esc, an external abort, or a newer MCP flow are neutral
// cancellations. The timeout path also aborts the controller but does
// not set this flag, so it remains a surfaced error.
if (cancellationRequested) {
throw new MCPOAuthCancelledError();
}
const errorMsg = error instanceof Error ? error.message : String(error);
// Provide helpful error messages based on failure type
if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
throw new Error("OAuth flow timed out. Please try again.");
} else if (errorMsg.includes("403") || errorMsg.includes("unauthorized")) {
throw new Error("OAuth authorization failed. Please check your client credentials.");
} else if (errorMsg.includes("invalid_grant")) {
throw new Error("OAuth authorization code is invalid or expired. Please try again.");
} else if (errorMsg.includes("ECONNREFUSED") || errorMsg.includes("fetch failed")) {
throw new Error("Could not connect to OAuth server. Please check the URLs and your network connection.");
} else {
throw new Error(`OAuth authentication failed: ${errorMsg}`);
}
} finally {
this.ctx.editor.onEscape = originalOnEscape;
externalSignal?.removeEventListener("abort", onExternalAbort);
manualInputClaim?.clear("Manual MCP OAuth input cleared");
flowClaim.release();
}
}
/**
* Fold a completed OAuth flow back into a server config. Owns the
* persistence policy in one place: the auth block records the credential
* pointer plus refresh material, the oauth block echoes the client id for
* pre-auth reuse, and only a user-supplied client secret is ever written —View on GitHub (pinned to 9690622007)
Solutions
- Restart the OAuth flow from scratch to get a fresh authorization code — codes are single-use
- Do not reuse or re-run with a previously captured authorization code
- Check client clock synchronization if the provider enforces strict expiry
- Ensure redirect_uri and client_id in the token exchange exactly match the authorization request
Defensive patterns
Strategy: retry
Try / catch
try {
await runMcpOAuthFlow();
} catch (err) {
if (err instanceof Error && err.message.includes('invalid or expired')) {
await runMcpOAuthFlow(); // fresh flow issues a fresh single-use code
}
} Prevention
- Never replay authorization codes — each code is single-use
- Start the token exchange immediately after receiving the code
- Ensure redirect_uri and client_id match exactly between authorize and token requests
- Keep client clocks synchronized (NTP)
When it happens
Trigger: Replaying an authorization code that was already redeemed; the code expired because the token exchange happened too late; clock skew between client and server; the code was issued for a different redirect_uri or client_id than the exchange used.
Common situations: Retrying the flow and the callback server reusing a stale code; user pasting an old manual authorization code; provider with very short code lifetimes and a slow network.
Related errors
- MCP OAuth credential is missing refresh material
- Broker returned non-OAuth credential for ${provider}
- Token exchange returned no access token${providerError ? `:
- MCP OAuth refresh failed: ${response.status} ${text}
- HTTP ${response.status} resuming MCP SSE stream: auth refres
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/d15d9d6e4483d6f7.
Report an issue: GitHub.