How ChatMCP Handles Authentication with Different LLM Providers: API Keys vs. OAuth 2.0

ChatMCP supports dual authentication models: static API keys for cloud LLM providers (OpenAI, Claude, etc.) and dynamic OAuth 2.0 with PKCE for self-hosted MCP servers, with credentials stored separately in user settings and per-server JSON configs respectively.

The daodao97/chatmcp repository implements a clean separation between these two authentication paths. API key flows are managed by the LLM client layer, while OAuth flows utilize a dedicated discovery service and web-based PKCE handler. This architecture allows seamless integration with both commercial LLM APIs and custom Model Context Protocol (MCP) endpoints.

API Key Authentication for Cloud LLM Providers

For commercial providers like OpenAI, Anthropic, and Google, ChatMCP uses straightforward bearer token authentication. The implementation spans three layers: settings persistence, client factory instantiation, and request-level header injection.

Storing Credentials in LLMProviderSetting

User API keys are persisted in the LLMProviderSetting class within lib/provider/settings_provider.dart. This model stores the raw key alongside provider metadata.

class LLMProviderSetting {
  final String providerId;   // e.g., "openai", "claude", "gemini"
  final String apiKey;       // user-entered key
  final String apiEndpoint;  // optional custom endpoint
}

The settings UI (lib/page/setting/llm_setting.dart) allows users to edit these values, which are then serialized to the app's local settings JSON file.

Building Authenticated Clients via LLMFactory

The factory method LLMFactory.create in lib/llm/llm_factory.dart instantiates provider-specific clients using the stored credentials. It accepts the provider enum, API key, and base URL, then returns concrete implementations like OpenAIClient or ClaudeClient.

static BaseLLMClient create(LLMProvider provider,
    {required String apiKey, required String baseUrl, String? apiVersion}) {
  switch (provider) {
    case LLMProvider.openai:
      return OpenAIClient(apiKey: apiKey, baseUrl: baseUrl);
    case LLMProvider.claude:
      return ClaudeClient(apiKey: apiKey, baseUrl: baseUrl);
    // Additional providers...
  }
}

Injecting Bearer Tokens in HTTP Headers

Each LLM client constructs a static _headers map that includes the Authorization: Bearer scheme. In lib/llm/openai_client.dart, this pattern is implemented as:

final _headers = {
  'Content-Type': 'application/json; charset=utf-8',
  'Authorization': 'Bearer $apiKey',
};

All HTTP requests—whether for chatCompletion or streaming responses—pass these headers directly to the underlying http client, ensuring the remote LLM validates the request on every call.

OAuth 2.0 Authentication for MCP Servers

Self-hosted or third-party MCP servers often require full OAuth 2.0 Authorization Code flows. ChatMCP automates this via PKCE (Proof Key for Code Exchange), discovery services, and token persistence in server-specific configuration files.

Auto-Discovering OAuth Endpoints

The OAuthDiscoveryService in lib/utils/oauth_discovery.dart attempts three strategies to locate OAuth metadata:

  1. RFC 8414 well-known endpoint: /.well-known/oauth-authorization-server
  2. Direct server probe: Inspecting SSE endpoint headers for WWW-Authenticate hints
  3. Custom metadata path: /oauth/metadata

Upon success, it returns an OAuthDiscoveryResult containing authorizationUrl, tokenUrl, clientId, scope, and redirectUri. This enables dynamic configuration without manual endpoint entry.

Executing the PKCE Flow

For web platforms, WebOAuthHandler in lib/utils/oauth_web.dart manages the OAuth handshake. The startOAuthFlow method generates a PKCE code verifier and challenge, then opens a popup window to the authorization endpoint.

final codeVerifier = _generateRandomString(128);
final codeChallenge = _generateCodeChallenge(codeVerifier);
final authUri = Uri.parse(authorizationUrl).replace(queryParameters: {
  'response_type': 'code',
  'client_id': clientId,              // omitted for public clients
  'redirect_uri': redirectUri,
  'scope': scope,
  'code_challenge': codeChallenge,
  'code_challenge_method': 'S256',
});
final popup = html.window.open(authUri.toString(), 'oauth_popup', ...);

The handler listens for postMessage events from web/oauth_callback.html (the redirect page) and validates the state parameter to prevent CSRF attacks before extracting the authorization code.

Token Exchange and Storage

Once the user authorizes access, WebOAuthHandler.exchangeCodeForToken performs the token exchange via POST request:

  • Grant type: authorization_code
  • PKCE verifier: Included to validate the challenge
  • Client ID: Included only for confidential clients

In lib/provider/mcp_server_provider.dart, the autoAuthenticateServer method orchestrates the full flow: discovery, authorization, exchange, and persistence. Tokens are stored in assets/mcp_server.json under an oauth object:

"oauth": {
  "enabled": true,
  "client_id": "...",
  "authorization_url": "...",
  "token_url": "...",
  "scope": "...",
  "redirect_uri": "...",
  "access_token": "...",
  "refresh_token": "...",
  "token_expiry": "2026-03-01T12:34:56Z"
}

Subsequent API calls to that MCP server automatically include Authorization: Bearer <access_token> until expiry, at which point the WebOAuthHandler.refreshToken method can renew the session using the stored refresh token.

Runtime Authentication Flow

When a chat session initializes, ChatMCP resolves authentication through the following sequence:

  1. Provider resolution: ChatPage queries ProviderManager.chatModelProvider to retrieve the active LLMProviderSetting
  2. Client instantiation: LLMFactory.create builds the client with the API key from settings
  3. OAuth check: If the selected server requires OAuth (requiresOAuth == true), McpServerProvider.discoverOAuthForServer runs discovery
  4. Conditional authentication: If no valid token exists in assets/mcp_server.json, autoAuthenticateServer launches the web flow
  5. Request execution: All LLM and MCP requests include the appropriate Authorization header—either the static API key or the dynamic OAuth bearer token

Summary

  • API key authentication stores credentials in LLMProviderSetting (lib/provider/settings_provider.dart) and injects them as Authorization: Bearer headers via provider-specific clients created by LLMFactory.create.
  • OAuth authentication uses OAuthDiscoveryService (lib/utils/oauth_discovery.dart) to auto-discover endpoints and WebOAuthHandler (lib/utils/oauth_web.dart) to execute PKCE flows.
  • Token persistence saves OAuth credentials to per-server JSON configs (assets/mcp_server.json), while API keys reside in the main user settings file.
  • Platform support for OAuth is currently web-only, utilizing web/oauth_callback.html for secure redirect handling via postMessage.

Frequently Asked Questions

How does ChatMCP store API keys securely?

API keys are stored as plaintext in the local LLMProviderSetting model within the user's settings JSON. While the app persists these values locally, it does not implement additional encryption at rest; security relies on the host operating system's file permissions. For OAuth tokens, the application stores access and refresh tokens in the per-server configuration file at assets/mcp_server.json.

Can ChatMCP automatically detect OAuth configuration for any MCP server?

Yes, via the OAuthDiscoveryService in lib/utils/oauth_discovery.dart. The service attempts RFC 8414 well-known endpoint discovery, direct SSE endpoint probing, and custom metadata paths. If the server exposes standard OAuth discovery documents, ChatMCP can automatically configure the authorization and token URLs without manual user input.

What authentication methods are supported for local LLMs like Ollama?

Local LLM providers such as Ollama use the same API key architecture as cloud providers, though they typically require empty or dummy keys. The LLMFactory.create method instantiates an OllamaClient with the provided base URL (usually http://localhost:11434), and the client injects the key into headers even if the local server ignores it.

Is the OAuth PKCE flow available on desktop platforms?

Currently, the PKCE implementation in lib/utils/oauth_web.dart is web-specific, utilizing dart:html for popup windows and postMessage communication with web/oauth_callback.html. Desktop platforms would require a local HTTP redirect URI handler or embedded browser implementation, which is not present in the current codebase.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →