---
title: "MCP OAuth and security"
description: "OAuth 2.1 with PKCE and Dynamic Client Registration for ComStack MCP connections. Token flow, refresh rotation, and connector status."
url: "https://comstack.ai/docs/mcp/oauth/"
---
## 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:

```text
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

| 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

```text
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 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_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 publish** — `publish` 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:

| 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](/docs/mcp.md) — install, tools, and safeguards
- [MCP errors and recovery](/docs/mcp/errors.md) — the full auth + publish error reference
- [API authentication](/docs/api/authentication.md) — Firebase ID tokens, API keys, and OAuth bearer from the caller perspective
