Skip to content

Commit 528966d

Browse files
feat(auth): add personal access token (pat) support and docs
add support for plurality personal access tokens (prefixed with plur_pat_) in jwt auth middleware: verify tokens via backend pat verification endpoint, enforce mcp:tools scope, set current token/user, and return appropriate errors. introduce pat prefix constant and backend api url import. document authentication options and pat usage in README with creation, header format, and token characteristics.
1 parent bd0550c commit 528966d

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,29 @@ Production URL: `https://app.plurality.network/mcp`
118118

119119
Dev URL: `https://dev.plurality.network/mcp`
120120

121+
### Authentication — choose your method
122+
123+
The MCP server accepts **two** auth methods:
124+
125+
| Method | When to use | Browser required? |
126+
|---|---|---|
127+
| **OAuth 2.1 + PKCE** (Hydra) | Interactive clients: Claude Desktop, Web, Code, ChatGPT | Yes (one-time) |
128+
| **Personal Access Token (PAT)** | Headless agents, CI runners, custom integrations, Perplexity, n8n, LangChain | No |
129+
130+
PATs require a **paid plan** and are managed from the **Connect via MCP** popup in the dashboard sidebar (click "Manage tokens →"). Pick OAuth if your client supports a browser; pick PAT if it doesn't.
131+
132+
#### Using a PAT
133+
134+
1. Sign in to the dashboard, open **Connect via MCP → Manage tokens**
135+
2. Click **Create token**, give it a name and optional expiry, copy the `plur_pat_…` value
136+
3. Configure your client to send it as a Bearer token:
137+
```
138+
Authorization: Bearer plur_pat_...
139+
```
140+
4. Server URL is the same as for OAuth: `https://app.plurality.network/mcp`
141+
142+
PATs auto-revoke at their expiry, can be rotated with a configurable grace period (default 7 days), and can be immediately revoked from the dashboard. They never appear in logs and are stored hashed.
143+
121144
### Claude Desktop / Web
122145

123146
**Easy setup (paid plans — Pro, Max, Team, Enterprise):**

src/plurality_mcp_server/auth.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import time
44
import httpx
55
import jwt as pyjwt
6-
from plurality_mcp_server.config import http_client, HYDRA_ISSUER, MCP_RESOURCE_URL, current_token, current_user_id
6+
from plurality_mcp_server.config import http_client, HYDRA_ISSUER, MCP_RESOURCE_URL, BACKEND_API_URL, current_token, current_user_id
7+
8+
# ── PAT prefix used by Plurality personal access tokens ──
9+
PAT_PREFIX = "plur_pat_"
710

811
# ── JWKS cache for local JWT validation ──
912
_jwks_cache: dict = {"keys": [], "fetched_at": 0}
@@ -147,6 +150,50 @@ async def __call__(self, scope, receive, send):
147150

148151
token = auth_header.removeprefix("Bearer ").strip()
149152

153+
# ── Personal Access Token (PAT) path ──
154+
# Validates via the backend (which owns the PAT DB). Keeps this server stateless.
155+
if token.startswith(PAT_PREFIX):
156+
try:
157+
verify_resp = await http_client.get(
158+
f"{BACKEND_API_URL}/pat/tokens/verify",
159+
headers={"Authorization": f"Bearer {token}"},
160+
timeout=5.0,
161+
)
162+
except httpx.RequestError as e:
163+
await self._send_json_error(scope, receive, send, 502, {
164+
"error": "pat_verify_failed",
165+
"message": f"Failed to verify PAT with backend: {str(e)}",
166+
})
167+
return
168+
169+
if verify_resp.status_code != 200:
170+
err_body = {}
171+
try:
172+
err_body = verify_resp.json()
173+
except Exception:
174+
pass
175+
await self._send_json_error(scope, receive, send, 401, {
176+
"error": err_body.get("error", "invalid_pat"),
177+
"message": "Invalid or expired API token",
178+
}, {"WWW-Authenticate": f'Bearer resource_metadata="{MCP_RESOURCE_URL}/.well-known/oauth-protected-resource"'})
179+
return
180+
181+
info = verify_resp.json()
182+
pat_scopes = info.get("scopes", [])
183+
if "mcp:tools" not in pat_scopes:
184+
await self._send_json_error(scope, receive, send, 403, {
185+
"error": "insufficient_scope",
186+
"message": "Token missing required scope: mcp:tools",
187+
}, {"WWW-Authenticate": f'Bearer error="insufficient_scope", scope="mcp:tools"'})
188+
return
189+
190+
user_id = info.get("user_id", "")
191+
print(f"[AUTH-PAT] {method} {path} | user={user_id} | token=...{token[-8:]}", flush=True)
192+
current_token.set(token)
193+
current_user_id.set(user_id)
194+
await self.app(scope, receive, send)
195+
return
196+
150197
try:
151198
decoded = await verify_jwt(token)
152199
user_id = decoded.get("sub", "")

0 commit comments

Comments
 (0)