Skip to content

fix: OAuth consent owner-auth + shared MCP memory across agents#45

Merged
imonroe merged 3 commits into
mainfrom
claude/zen-babbage-eOZ3p
May 23, 2026
Merged

fix: OAuth consent owner-auth + shared MCP memory across agents#45
imonroe merged 3 commits into
mainfrom
claude/zen-babbage-eOZ3p

Conversation

@imonroe
Copy link
Copy Markdown
Owner

@imonroe imonroe commented May 23, 2026

Two fixes.

1. Authenticate the resource owner at OAuth consent (security)

POST /oauth/authorize issued an authorization code to anyone who clicked "Authorize" — no resource-owner authentication. Since the OAuth endpoints are public, any party who knew the server URL could add it as a connector in their own Claude, complete consent, and get a token with full access to the single user's memories. URL obscurity was the only protection.

Fix: require MEM0_API_KEY at consent. The page has an API-key field; POST /oauth/authorize validates it (constant-time, rejects an empty configured key) before issuing a code, and re-renders with a 401 on failure. Reuses the existing master credential — no new env var.

2. Make MCP memory reads span all agents (shared store)

Diagnosed live: with both Claude Code and Codex connected, memory wasn't shared. A GET /api/v1/memories dump showed all memories under one user_id (so the store is shared) but one tagged agent_id="claude-web" — i.e. clients' models populate agent_id. The MCP search_memories/list_memories tools filtered by agent_id when supplied, so each agent read only its own writes.

Fix: remove agent_id from the MCP read tools (search/list) so reads always span the whole user store. agent_id stays on add_memory as a write-only provenance tag. The REST API keeps agent_id read filters for explicit scripted use.

Tests

  • OAuth: test_authorize_rejects_wrong_password, test_authorize_rejects_missing_password (401, no code issued); existing flow tests supply the key.
  • MCP: search_memories asserts filters == {"user_id": "ian"} (no agent scoping); test_read_tools_do_not_expose_agent_id locks agent_id out of the read tools' schemas.
  • 69 tests pass; ruff clean.

Docs

  • User Guide: enter MEM0_API_KEY at consent (+ security rationale); agent_id is write-only over MCP.
  • Developer Guide + CLAUDE.md: consent-auth invariant and shared-read (no agent_id filtering) invariant.

Not included

spaCy/fastembed deps intentionally skipped (heavy footprint for optional NLP/BM25; OpenAI semantic search is sufficient).

https://claude.ai/code/session_01U3EtN3puoZRq2t7nedcnHY

The /oauth/authorize consent step issued an authorization code to anyone
who clicked "Authorize", with no owner authentication. Because the OAuth
endpoints are public, any party who knew the server URL could add it as a
custom connector, complete consent, and obtain a token with full access
to the single user's memories.

Require MEM0_API_KEY at the consent step: the form now has an API-key
field and POST /oauth/authorize validates it (constant-time) before
issuing a code, re-rendering the form with a 401 on failure. Add tests
for the wrong/missing password cases and document the security model.

https://claude.ai/code/session_01U3EtN3puoZRq2t7nedcnHY
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR closes an OAuth Phase 2 authorization vulnerability by requiring resource-owner authentication at the consent step, reusing MEM0_API_KEY as the gate before issuing authorization codes.

Changes:

  • Add an API-key password field to the OAuth consent page and require MEM0_API_KEY (constant-time compare) on POST /oauth/authorize.
  • Update OAuth tests to supply the key and add negative tests ensuring no code is issued on wrong/missing key.
  • Update user/developer documentation to describe the new consent authentication invariant.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
app/oauth.py Adds consent-page rendering helper and enforces MEM0_API_KEY check before issuing authorization codes.
tests/test_oauth.py Updates authorize flow tests to include the API key; adds 401 rejection tests for wrong/missing key.
docs/USER_GUIDE.md Updates Claude.ai/Cowork OAuth instructions to include entering MEM0_API_KEY at consent, with a security note.
docs/DEVELOPER_GUIDE.md Documents that /oauth/authorize POST authenticates the resource owner via MEM0_API_KEY.
CLAUDE.md Records the consent-authentication invariant as a “don’t remove” security property.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/oauth.py
Comment on lines 235 to 237
client = oauth_store.get_client(client_id)
if not client or redirect_uri not in client["redirect_uris"]:
raise HTTPException(status_code=400, detail="invalid client or redirect_uri")
Comment thread docs/USER_GUIDE.md
This requires **Phase 2** (`OAUTH_SIGNING_KEY` set). In the client's connector settings:

1. Add a **custom connector** pointing at `https://mem0.your-domain.com/mcp/`.
1. Add a **custom connector** pointing at `https://mem0.your-domain.com/mcp`.
The MCP search_memories/list_memories tools filtered by agent_id when the
caller supplied it. Clients' models populate agent_id (observed: a memory
tagged agent_id="claude-web"), so each agent effectively read only its own
writes — Claude Code and Codex appeared to have separate memories even
though the store is physically shared under one user_id.

Remove agent_id from the read tools so search/list always span the whole
user store. Keep agent_id on add_memory as an optional write-only
provenance tag. The REST API still supports agent_id read filters for
explicit scripted use.

https://claude.ai/code/session_01U3EtN3puoZRq2t7nedcnHY
@imonroe imonroe changed the title fix: authenticate resource owner at OAuth authorize consent fix: OAuth consent owner-auth + shared MCP memory across agents May 23, 2026
@imonroe imonroe requested a review from Copilot May 23, 2026 21:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Comment thread app/oauth.py Outdated
Comment on lines +196 to +201
def _owner_authenticated(password: str) -> bool:
# Single-user: the resource owner proves ownership at the consent step with
# the same MEM0_API_KEY that protects the API. Constant-time compare; an empty
# configured key must never authenticate.
key = get_settings().mem0_api_key
return bool(key) and secrets.compare_digest(password, key)
Comment thread docs/USER_GUIDE.md
This requires **Phase 2** (`OAUTH_SIGNING_KEY` set). In the client's connector settings:

1. Add a **custom connector** pointing at `https://mem0.your-domain.com/mcp/`.
1. Add a **custom connector** pointing at `https://mem0.your-domain.com/mcp`.
Comment thread docs/USER_GUIDE.md
This requires **Phase 2** (`OAUTH_SIGNING_KEY` set). In the client's connector settings:

1. Add a **custom connector** pointing at `https://mem0.your-domain.com/mcp/`.
1. Add a **custom connector** pointing at `https://mem0.your-domain.com/mcp`.
Comment thread docs/DEVELOPER_GUIDE.md
Comment on lines 123 to +127
The OAuth flow (`app/oauth.py`) is OAuth 2.1 with PKCE (S256 required) and public clients only — no
client secrets are issued. Endpoints: `/oauth/register` (DCR), `/oauth/authorize` (GET form + POST
client secrets are issued. The `/oauth/authorize` consent step **authenticates the resource owner**:
the POST handler requires the `MEM0_API_KEY` (constant-time compared) before issuing a code. Without
this gate, anyone who reached the public consent screen could mint a token for the single user's
memories just by clicking "Authorize". Endpoints: `/oauth/register` (DCR), `/oauth/authorize` (GET form + POST
Comment thread CLAUDE.md
- **`MEM0_EMBED_DIMS` must match the embedder's real output dimension** (3-small=1536, 3-large=3072). A mismatch causes *silent* search failures, not errors. Changing embedding models requires dropping and recreating the Qdrant collection.
- **FastMCP = the PrefectHQ `fastmcp` PyPI package**, imported `from fastmcp import FastMCP`. It is NOT the older `mcp.server.fastmcp` module.
- **Same `MEM0_API_KEY` protects both** the REST endpoints (`require_bearer` dependency) and the MCP endpoint (`StaticTokenVerifier` in Phase 1).
- **The Phase 2 OAuth `/oauth/authorize` consent step authenticates the resource owner** by requiring `MEM0_API_KEY` (constant-time compare) before issuing a code. Don't remove this — the OAuth endpoints are public, so without it anyone reaching the consent screen could mint a token for the single user's memories.
…nsistency

Address review feedback on PR #45:
- POST /oauth/authorize now rejects an empty code_challenge (400) so a
  direct POST can't mint a code that could never be exchanged
- _owner_authenticated hashes both the submitted password and the
  configured key to fixed-length digests before secrets.compare_digest,
  so the comparison is genuinely constant-time (no length leak)
- standardize the Claude Code/Desktop example URLs on the canonical /mcp
  (no trailing slash) to match the OAuth section

https://claude.ai/code/session_01U3EtN3puoZRq2t7nedcnHY
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@imonroe imonroe merged commit 7dfc5f7 into main May 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants