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

> Discover how ChatMCP handles LLM authentication using API keys for cloud providers and OAuth 2.0 for self-hosted servers. Securely connect your LLMs with ease.

- Repository: [刀刀/chatmcp](https://github.com/daodao97/chatmcp)
- Tags: how-to-guide
- Published: 2026-02-28

---

**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](https://github.com/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.

```dart
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`.

```dart
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:

```dart
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.

```dart
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`](https://github.com/daodao97/chatmcp/blob/main/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`](https://github.com/daodao97/chatmcp/blob/main/assets/mcp_server.json) under an `oauth` object:

```json
"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`](https://github.com/daodao97/chatmcp/blob/main/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`](https://github.com/daodao97/chatmcp/blob/main/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`](https://github.com/daodao97/chatmcp/blob/main/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`](https://github.com/daodao97/chatmcp/blob/main/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`](https://github.com/daodao97/chatmcp/blob/main/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.