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

  1. Restart the OAuth flow from scratch to get a fresh authorization code — codes are single-use
  2. Do not reuse or re-run with a previously captured authorization code
  3. Check client clock synchronization if the provider enforces strict expiry
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/d15d9d6e4483d6f7. Report an issue: GitHub.