MCP OAuth and security
Overview
ComStack’s API uses OAuth 2.1 with PKCE (RFC 7636), Dynamic Client Registration (RFC 7591), and the discovery metadata format required by the MCP specification (RFC 8414). This page describes the authorization flow, token storage model, the roles and safeguards that protect a live site, and the connector status endpoints that MCP clients can use to check connection state.
Discovery
Two discovery endpoints expose machine-readable metadata:
GET /.well-known/oauth-authorization-serverGET /.well-known/oauth-protected-resourceBoth return JSON describing the issuer, the four endpoint URLs, and the supported PKCE method (S256 only).
Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/oauth/register | POST | Dynamic Client Registration. Returns a client_id — no client secret (public client). |
/oauth/authorize | GET | Renders the login UI. |
/oauth/authorize/complete | POST | Called by the login UI after sign-in completes. Issues an authorization code and redirects. |
/oauth/token | POST | Exchanges an authorization code or refresh token for an access token. |
All /oauth/* requests are subject to a rate limit of 30 requests per minute per IP.
Authorization flow
client -> /oauth/register -> { client_id }client -> /oauth/authorize?... -> login UI rendereduser signs inlogin UI -> /oauth/authorize/complete -> { code }, redirect to redirect_uriclient -> /oauth/token (code) -> { access_token, refresh_token }client -> POST /mcp with Bearer -> MCP trafficToken storage
All tokens are stored as sha256(rawValue) — the plaintext value is only known to the holder.
| Token type | TTL | Notes |
|---|---|---|
| Authorization code | 60 s | Single-use; deleted on exchange. |
| Access token | 1 h | Validated on every /mcp request. |
| Refresh token | 30 d | Rotated on each use (see below). |
| Publish confirmation token | 5 min | Single-use. |
Refresh rotation and family revoke
Each refresh-token redemption issues a new refresh token and records the previous one as rotated. If a previously-rotated token is presented again, the entire token family is revoked immediately. This protects against replay attacks where an attacker captures a refresh token after the legitimate client has already rotated it.
Roles and publish safeguards
OAuth identifies who you are; your role on each project decides what you can do. The ladder is none < guest < member < manager < admin, resolved per project and cached for about 30 seconds. The tools/list response is filtered by role: members see read and create tools; managers add the destructive and publishing tools; platform admins add template management. (There are no editor/viewer roles.)
Two extra guards protect the live site:
project_nameecho — destructive tools (publish,delete-page,update-theme,upload-custom-page, and similar) require the exact project name fromget-project-state(compared case-insensitively, trimmed). A mismatch is rejected before any write.- Two-step publish —
publishis a dry run that only returns a diff manifest plus a single-use, 5-minute confirmation token; nothing deploys untilpublish-confirm, which re-checks that no draft or theme drifted since the dry run. Both require themanagerrole.
Connector status
The Account page surfaces a connection indicator for MCP clients. Two HTTP endpoints support this — these are REST endpoints, not MCP protocol messages:
| Endpoint | Method | Description |
|---|---|---|
/api/account/connectors/claude/status | GET | Returns { connected, last_seen_at }. Connected when the caller owns at least one unexpired access token for a Claude MCP client. |
/api/account/connectors/claude | DELETE | Revokes all active access and refresh tokens for Claude MCP clients on this account. The user must re-authorize inside Claude to reconnect. |
Common errors
invalid_client — the client_id is unrecognized or belongs to a different user context. Re-register using /oauth/register to obtain a fresh client_id.
invalid_grant on token exchange — the authorization code expired (60 s TTL) or was already used. Restart the flow from /oauth/authorize.
invalid_grant on refresh — the refresh token was rotated or revoked. If a rotated token is replayed, the entire family is revoked. The user must re-authorize.
Rate limit exceeded — HTTP 429 when calling any /oauth/* endpoint more than 30 times per minute from one IP. Back off and retry with exponential delay.
Related
- MCP server overview — install, tools, and safeguards
- MCP errors and recovery — the full auth + publish error reference
- API authentication — Firebase ID tokens, API keys, and OAuth bearer from the caller perspective