Skip to content

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-server
GET /.well-known/oauth-protected-resource

Both return JSON describing the issuer, the four endpoint URLs, and the supported PKCE method (S256 only).

Endpoints

EndpointMethodPurpose
/oauth/registerPOSTDynamic Client Registration. Returns a client_id — no client secret (public client).
/oauth/authorizeGETRenders the login UI.
/oauth/authorize/completePOSTCalled by the login UI after sign-in completes. Issues an authorization code and redirects.
/oauth/tokenPOSTExchanges 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 rendered
user signs in
login UI -> /oauth/authorize/complete -> { code }, redirect to redirect_uri
client -> /oauth/token (code) -> { access_token, refresh_token }
client -> POST /mcp with Bearer -> MCP traffic

Token storage

All tokens are stored as sha256(rawValue) — the plaintext value is only known to the holder.

Token typeTTLNotes
Authorization code60 sSingle-use; deleted on exchange.
Access token1 hValidated on every /mcp request.
Refresh token30 dRotated on each use (see below).
Publish confirmation token5 minSingle-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_name echo — destructive tools (publish, delete-page, update-theme, upload-custom-page, and similar) require the exact project name from get-project-state (compared case-insensitively, trimmed). A mismatch is rejected before any write.
  • Two-step publishpublish is a dry run that only returns a diff manifest plus a single-use, 5-minute confirmation token; nothing deploys until publish-confirm, which re-checks that no draft or theme drifted since the dry run. Both require the manager role.

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:

EndpointMethodDescription
/api/account/connectors/claude/statusGETReturns { connected, last_seen_at }. Connected when the caller owns at least one unexpired access token for a Claude MCP client.
/api/account/connectors/claudeDELETERevokes 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.

Last updated: