MCP Authorization And Bearer Compatibility Plan
Goal
Protect ai-memory-hub MCP and HTTP access beyond local testing. Use MCP-compliant OAuth resource-server mode for protected MCP deployments, while retaining bearer-token mode as a local/LAN compatibility mode for existing HTTP API and personal-token workflows.
Implemented modes:
api.auth: nonefor CI, test fixtures, and maintainer-only local smoke tests. Do not present this as a user setup option.api.auth: bearer_tokenfor compatibility with simple personal access-token deployments. This is useful for local/LAN HTTP API workflows, but it is not the recommended MCP-compliant release posture.api.auth: oauth_resource_serverfor MCP-compliant OAuth authorization.
Source: https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
Recommendation
For first-release user guidance, document api.auth: oauth_resource_server as
the MCP setup path. Keep api.auth: none available for CI/test fixtures and
maintainer smoke tests only, and keep bearer_token implemented and tested as a
compatibility mode, but do not present either as the recommended MCP setup.
Both bearer-token compatibility mode and OAuth resource-server mode use the standard authorization header shape:
Authorization: Bearer <token>
Do not implement X-API-Key.
Do not put user keys in config.yaml. Store users and token hashes in the
metadata database so an admin CLI/API/UI can create, revoke, expire, and rotate
tokens without editing config.
Use api.auth: none only when:
- Running in CI.
- Running maintainer-only local smoke tests, bound to loopback, for example
127.0.0.1:8000.
Do not list api.auth: none as a normal user setup option. It exists so tests,
fixtures, and low-level local debugging can run without an external identity
provider.
- Running isolated smoke tests where the service is not exposed to other hosts.
Use api.auth: oauth_resource_server before exposing MCP outside loopback. Use
HTTPS, VPN, SSH tunnel, or a trusted reverse proxy if the endpoint crosses
machines. Reserve bearer_token for compatibility scenarios where the operator
accepts that it is not a full MCP OAuth resource-server deployment.
Current Status
Implemented:
- [x] Config placeholders exist:
api.auth: noneapi.auth: bearer_token- [x] Docs warn not to expose the API/MCP endpoint beyond localhost without auth.
- [x] MCP and HTTP API are both mounted by the same FastAPI app.
- [x] Auth enforcement middleware protects
/memory/*and/mcp/*. - [x] SQLite and Postgres metadata providers store users and token hashes.
- [x] Conversations and facts are stamped with server-derived
owner_id. - [x] Search, retrieve, ask, fact search, profile, and fact supersession are
scoped by
owner_id. - [x] Vector candidates are filtered through metadata ownership before returning or answering.
Implemented since the initial bearer-token baseline:
- [x] Admin CLI commands for user, bearer-token, project, and membership management.
- [x] Project workspace membership for shared collaboration. See
project_workspace_collaboration_plan.md. - [x] MCP OAuth protected resource metadata.
- [x] MCP
WWW-Authenticatechallenges withresource_metadataand scope hints. - [x] OAuth access-token validation with audience/resource binding for configured HS256 JWTs.
- [x] Scope checks for read/write route groups.
- [x] Tests for HTTP API and
/mcp/.
Threat Model
This plan protects against:
- Other devices reading or writing memory when the service is exposed beyond loopback.
- Accidental unauthenticated exposure through
0.0.0.0:8000. - Drive-by calls to
/memory/*and/mcp/. - MCP clients accidentally sending credentials through unsupported paths.
- Access tokens issued for other resources being reused against ai-memory-hub.
- One valid user reading or writing another user's memory.
This plan does not protect against:
- A compromised trusted client machine.
- Authorization server compromise.
- Token leakage in client config, shell history, logs, or traces.
- Internet exposure without TLS.
Internet-facing MCP must use HTTPS and api.auth: oauth_resource_server, or run
behind an identity-aware reverse proxy that performs equivalent OAuth
resource-server validation and forwards only trusted requests.
MCP Authorization Requirements
For HTTP-based MCP transports, the MCP spec says authorization is optional, but when supported the implementation should conform to the MCP authorization spec.
Server-side requirements that affect ai-memory-hub:
- [x] Treat the MCP server as an OAuth resource server.
- [x] Accept access tokens through
Authorization: Bearer <access-token>. - [x] Require authorization on every HTTP request to protected MCP endpoints.
- [x] Do not accept access tokens in URI query strings.
- [x] Validate tokens before processing MCP requests.
- [x] Validate that presented tokens were issued for this MCP server as the intended resource/audience.
- [x] Do not pass inbound MCP access tokens through to downstream services.
- [x] Return
401 Unauthorizedfor missing, invalid, or expired tokens. - [x] Return
403 Forbiddenfor valid tokens with insufficient scope. - [x] Include
WWW-Authenticate: Bearer ...challenges for auth failures. - [x] Expose OAuth Protected Resource Metadata.
- [x] Include a
resource_metadataURL inWWW-Authenticatechallenges. - [x] Prefer including a
scopeparameter in challenges so clients can request least-privilege access.
Discovery requirements:
- [x] Serve protected resource metadata at well-known URIs:
/.well-known/oauth-protected-resource/.well-known/oauth-protected-resource/mcpwhen/mcpidentifies the MCP resource.- [x] Metadata includes
authorization_serverswith at least one configured authorization server. - [x] Metadata includes the MCP server resource identifier.
- [x] Metadata advertises supported scopes.
Client-facing compatibility requirements:
- [x] The canonical MCP resource URI is configurable because loopback, reverse-proxy, LAN, and HTTPS deployments have different public URLs.
- [x] The canonical URI must be absolute and must not include a fragment.
- [x] Use the most specific stable MCP URI when the path matters, for example
https://memory.example.com/mcp.
Auth Modes
Config:
api:
auth: oauth_resource_server
public_base_url: "" # required for oauth_resource_server
token:
hash_secret_env: AMH_TOKEN_HASH_SECRET
default_expiry_days: 365
oauth:
authorization_servers: []
resource: "" # defaults to public_base_url + /mcp when unset
scopes_supported:
- memory:read
- memory:write
- memory:admin
Rules:
none: allow all requests. Use only for CI/test fixtures and maintainer-only loopback smoke tests; do not document it as a user setup path.bearer_token: require a server-issued personal access token in theAuthorization: Bearerheader, map it to a user, then scope every read/write to that user.oauth_resource_server: require MCP-compliant Bearer access tokens for protected routes.
Protected paths:
/memory/insert/memory/search/memory/retrieve/memory/ask/memory/facts/search/memory/profile/get/memory/facts/supersede/mcp/*
Public paths:
/health/ready/.well-known/oauth-protected-resource/.well-known/oauth-protected-resource/*/docs,/openapi.json, and/redocshould be configurable. For production, protect or disable them.
P0: Simple Bearer Tokens And Per-User Isolation
This is the first implementation target.
Implementation sequence:
- [x] Add config validation:
- [x] allowed
api.authvalues:none,bearer_token,oauth_resource_server - [x]
bearer_tokenrequires a token hash secret from environment or a generated local secret file outside the repo - [x] warn when
auth=noneis used with a non-loopback bind address - [x] Add metadata-store tables/records for auth:
- [x]
users - [x]
auth_tokens - [x] token hash, token id, token prefix, display name, created/expires/revoked timestamps
- [x] scopes and last-used timestamps
- [x] store only token hashes, never raw tokens
- [x] Add admin CLI commands:
- [x]
aim admin user create <user_id> [--display-name ...] - [x]
aim admin user list - [x]
aim admin token create --user <user_id> [--display-name ...] - [x]
aim admin token list --user <user_id> - [x]
aim admin token revoke <token_id_or_prefix> - [x] print raw token only once on creation
- [x] Add HTTP auth middleware:
- [x] parse
Authorization: Bearer <token> - [x] reject missing/invalid/revoked/expired tokens with
401 - [x] reject insufficient scope with
403 - [x] keep
/healthand/readypublic - [x] protect
/memory/*and/mcp/* - [x] reject or ignore tokens in query strings
- [x] Add request principal context:
- [x]
user_id - [x] token id
- [x] scopes
- [x] auth mode
- [x] Add per-user ownership:
- [x] stamp new conversations with server-side
owner_id - [x] stamp extracted facts with
owner_id - [x] do not trust client-supplied
metadata.owner_id - [x] filter search/retrieve/ask/fact/profile operations by
owner_id - [x] prevent direct retrieval of another user's memory by id
- [x] Add vector isolation:
- [x] include
owner_idin vector rows where provider supports metadata - [x] for providers without vector-side filters, filter candidates after metadata lookup before returning or answering
- [x] add tests proving no cross-user leakage through vector candidates
- [x] Add scopes:
- [x]
memory:readfor search/retrieve/ask/fact search/profile - [x]
memory:writefor validate/insert/fact supersession - [x]
memory:adminfor future admin UI/API - [x] Add redaction:
- [x] redact
Authorization: Bearer ...in logs and errors - [x] avoid returning token hashes
- [x] never log raw generated tokens
- [x] Add tests:
- [x] no-auth mode still passes existing tests
- [x] bearer mode rejects missing/invalid/revoked/expired token
- [x] bearer mode accepts valid token
- [x] user A cannot search/retrieve/ask user B memory
- [x] user A cannot access user B facts/profile
- [x] insufficient scope returns
403 - [x] admin CLI creates and revokes tokens
Acceptance criteria:
- A Raspberry Pi or LAN deployment can be protected with personal access tokens.
- Each token maps to exactly one user/principal.
- Every memory/fact read and write is scoped to the token's user.
- Raw tokens are shown once and only token hashes are stored.
- Existing CI/test-fixture
auth=nonebehavior remains available. - The MCP client request shape is already compatible with future OAuth mode.
OAuth Resource-Server Mode
Use api.auth: oauth_resource_server for user-facing MCP setup and whenever the
HTTP MCP endpoint is reachable outside CI/test fixtures.
Token validation should be adapter-based so the first implementation can support one practical provider without hardcoding the project to a specific IdP:
- JWT validation through JWKS.
- Token introspection through a configured authorization server.
- Identity-aware reverse proxy headers only when the proxy is trusted and strips spoofed inbound headers.
Required validation:
- [x] Signature validity for configured HS256 JWTs.
- [x] Expiration.
- [x] Issuer, when configured.
- [x] Audience/resource matches the configured MCP resource URI.
- [x] Required scopes for protected HTTP route groups and MCP access.
Current limitation: JWKS and introspection adapters are still planned. The first
implementation validates HS256 JWTs using api.oauth.jwt_secret or
api.oauth.jwt_secret_env.
For the planned Google sign-in and client setup UI, use
improvements/google_oauth_connect_ui_plan.md as the source of truth. That plan
keeps ai-memory-hub as the OAuth resource server and adds a small Connect UI
that lets Google prove user identity before ai-memory-hub issues hub-owned MCP
access tokens.
Do not:
- [x] Accept arbitrary third-party access tokens.
- [x] Accept tokens missing this MCP server in their audience/resource claim.
- [x] Forward inbound MCP access tokens to OpenAI, Ollama, databases, vector stores, or other upstream APIs.
- [x] Put tokens in query strings, logs, traces, or MCP tool payloads.
Recommended initial scopes:
memory:read: search, retrieve, ask, resource reads.memory:write: validate, insert, fact supersession.memory:admin: config, debug, or observability summaries if exposed.
Scope failures:
- Missing/invalid/expired token:
401withWWW-Authenticate. - Valid token but insufficient scope:
403withWWW-Authenticate: Bearer error="insufficient_scope", scope="...".
Implementation Plan
Phase 1: Config Validation
- [x] Add
APIConfig.authvalidator: - allowed:
none,bearer_token,oauth_resource_server - [x] Add model validators:
- if
auth=oauth_resource_server,public_base_urland at least one authorization server must be configured - if
oauth.resourceis set, it must be an absolute URI with no fragment auth=noneis allowed but should warn when binding to non-loopback hosts- [x] Remove or deprecate
api.api_keyfrom code and docs. - [x] Add tests in
tests/unit/test_config.py.
Phase 2: Auth Middleware
Add memory/api/auth.py:
is_public_path(path, config) -> boolinstall_auth_middleware(app, config)extract_bearer_token(request) -> str | Nonebuild_www_authenticate_challenge(request, config, scopes=None, error=None) -> strrequired_scopes_for_request(request) -> set[str]
Tasks:
- [x] Install middleware in
create_app()before routes are used. - [x] Protect mounted MCP app path
/mcp. - [x] Keep auth logic independent from ingestion code.
- [x] Return HTTP auth failures before MCP handling.
- [x] Return
WWW-Authenticateon protected-path401responses. - [x] Keep
/health,/ready, and protected-resource metadata public. - [x] Ignore or reject query-string tokens.
- [x] Do not support
X-API-Key.
Phase 3: Protected Resource Metadata
Add endpoints:
GET /.well-known/oauth-protected-resourceGET /.well-known/oauth-protected-resource/mcp
Metadata fields:
- [x] Resource identifier for the MCP server.
- [x]
authorization_servers. - [x]
scopes_supported. - [x] Optional service documentation URLs if useful.
Tasks:
- [x] Generate metadata from config.
- [x] Add tests for root metadata.
- [x] Add tests for
/mcppath metadata. - [x] Ensure metadata responses do not expose secrets.
Phase 4: OAuth Token Validation
Add a token-validator abstraction:
AccessTokenClaimsTokenValidationResultTokenValidatorJWKSJWTTokenValidator- Optional later:
IntrospectionTokenValidator
Tasks:
- [x] Validate signature for configured HS256 JWTs.
- [x] Validate expiration.
- [x] Validate issuer when configured.
- [x] Validate audience/resource against configured MCP resource URI.
- [x] Validate scopes for protected route groups and MCP access.
- [x] Reject token passthrough; inbound tokens are only for authorizing ai-memory-hub.
Phase 5: Scope Mapping
Initial route and MCP operation mapping:
- [x]
memory:read: /memory/search/memory/retrieve/memory/ask/memory/facts/search/memory/profile/get- MCP resource reads
- MCP search, retrieve, ask, fact search, and profile tools
- [x]
memory:write: /memory/insert/memory/facts/supersede- MCP validate, insert, and fact supersession tools
- [x]
memory:admin: - future protected observability, debug, config, or maintenance endpoints
When scope is insufficient:
- [x] Return
403. - [x] Include
WWW-Authenticate: Bearer error="insufficient_scope". - [x] Include the minimum required
scopevalue. - [x] Include
resource_metadata.
Phase 6: MCP Client Compatibility
MCP clients must send Authorization: Bearer <access-token> on every HTTP
request to protected MCP endpoints. Clients should discover authorization
servers from protected resource metadata and request tokens for the configured
MCP resource URI.
Before documenting exact client syntax, verify current Codex and opencode MCP authorization behavior against official docs or local client behavior.
Fallbacks for clients without MCP authorization support:
- Run ai-memory-hub bound to
127.0.0.1. - Use SSH tunnel from another machine.
- Use an identity-aware reverse proxy that handles OAuth and only forwards authenticated requests.
Phase 7: Compose Examples
Keep checked-in Compose examples unauthenticated only for maintainer local smoke tests bound to loopback. User-facing Compose and MCP setup docs should use OAuth resource-server mode.
Add a separate OAuth/proxy example instead of a shared-secret LAN example:
- [x] Example reverse proxy with TLS.
- [x] Example
api.auth: oauth_resource_serverconfig. - [x] Example protected resource metadata config.
- [x] Example JWKS or introspection validator config.
Do not document unauthenticated 0.0.0.0:8000 as a recommended mode.
Phase 8: Tests
HTTP API tests:
- [x]
auth=noneallows/memory/searchin test config. - [x]
auth=nonewith non-loopback bind emits a warning. - [x]
/healthremains public. - [x]
/readyremains public. - [x] Protected resource metadata remains public and secret-free.
MCP tests:
- [x]
/mcp/initialize rejects missing Bearer token when OAuth auth is enabled. - [x]
/mcp/initialize accepts valid Bearer auth. - [x]
/mcp/tools/list accepts valid Bearer auth and session id. - [x] Auth rejection happens before tool execution.
- [x] Query-string tokens are rejected or ignored.
- [x]
WWW-Authenticateincludesresource_metadata.
OAuth resource-server tests:
- [x] Valid token with correct audience/resource succeeds.
- [x] Valid token with wrong audience/resource fails with
401. - [x] Expired token fails with
401. - [x] Valid token without required scope fails with
403. - [x] Insufficient-scope response includes
error="insufficient_scope"and requiredscope. - [x] Inbound access token is not forwarded to provider calls.
Security tests:
- [x] Token never appears in logs.
- [x] Redaction catches
Authorization: Bearer <value>. - [x] Query-string token attempts do not leak through access logs.
Phase 9: Documentation
Update:
- README security section.
- Historical Postgres/PGVector compose handoff notes were replaced by
examples/local-stack. docs/agents.md.docs/mcp_plan.md.
Document one recommended MCP setup mode:
api:
auth: oauth_resource_server
public_base_url: "https://memory.example.com"
oauth:
authorization_servers:
- "https://auth.example.com"
resource: "https://memory.example.com/mcp"
Internet exposure without built-in OAuth:
Put ai-memory-hub behind TLS plus an identity-aware reverse proxy, or expose it only through a VPN. Do not publish unauthenticated MCP/API endpoints.
Acceptance Criteria
api.authsupportsnone,bearer_token, andoauth_resource_server.api.auth: noneremains available for CI/test fixtures and maintainer-only loopback smoke tests, but is not documented as a user setup option.api.auth: bearer_tokenprotects/memory/*and/mcp/*withAuthorization: Bearer <token>.api.auth: oauth_resource_serverexposes MCP protected resource metadata.- Protected MCP responses include proper Bearer challenges.
- OAuth mode validates token audience/resource before processing MCP requests.
- OAuth mode uses
401for missing/invalid tokens and403for insufficient scopes. - Existing no-auth local and CI tests keep passing.
- Codex/opencode OAuth behavior is verified before exact setup syntax is added to user-facing docs.
- No access token appears in logs, traces, MCP payloads, or test failure output.
- Docs do not recommend unauthenticated
0.0.0.0:8000.