Skip to content

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: none for CI, test fixtures, and maintainer-only local smoke tests. Do not present this as a user setup option.
  • api.auth: bearer_token for 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_server for 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: none
  • api.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-Authenticate challenges with resource_metadata and 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 Unauthorized for missing, invalid, or expired tokens.
  • [x] Return 403 Forbidden for valid tokens with insufficient scope.
  • [x] Include WWW-Authenticate: Bearer ... challenges for auth failures.
  • [x] Expose OAuth Protected Resource Metadata.
  • [x] Include a resource_metadata URL in WWW-Authenticate challenges.
  • [x] Prefer including a scope parameter 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/mcp when /mcp identifies the MCP resource.
  • [x] Metadata includes authorization_servers with 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 the Authorization: Bearer header, 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 /redoc should 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.auth values: none, bearer_token, oauth_resource_server
  • [x] bearer_token requires a token hash secret from environment or a generated local secret file outside the repo
  • [x] warn when auth=none is 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 /health and /ready public
  • [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_id in 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:read for search/retrieve/ask/fact search/profile
  • [x] memory:write for validate/insert/fact supersession
  • [x] memory:admin for 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=none behavior 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: 401 with WWW-Authenticate.
  • Valid token but insufficient scope: 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="...".

Implementation Plan

Phase 1: Config Validation

  • [x] Add APIConfig.auth validator:
  • allowed: none, bearer_token, oauth_resource_server
  • [x] Add model validators:
  • if auth=oauth_resource_server, public_base_url and at least one authorization server must be configured
  • if oauth.resource is set, it must be an absolute URI with no fragment
  • auth=none is allowed but should warn when binding to non-loopback hosts
  • [x] Remove or deprecate api.api_key from 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) -> bool
  • install_auth_middleware(app, config)
  • extract_bearer_token(request) -> str | None
  • build_www_authenticate_challenge(request, config, scopes=None, error=None) -> str
  • required_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-Authenticate on protected-path 401 responses.
  • [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-resource
  • GET /.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 /mcp path metadata.
  • [x] Ensure metadata responses do not expose secrets.

Phase 4: OAuth Token Validation

Add a token-validator abstraction:

  • AccessTokenClaims
  • TokenValidationResult
  • TokenValidator
  • JWKSJWTTokenValidator
  • 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 scope value.
  • [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_server config.
  • [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=none allows /memory/search in test config.
  • [x] auth=none with non-loopback bind emits a warning.
  • [x] /health remains public.
  • [x] /ready remains 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-Authenticate includes resource_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 required scope.
  • [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.auth supports none, bearer_token, and oauth_resource_server.
  • api.auth: none remains available for CI/test fixtures and maintainer-only loopback smoke tests, but is not documented as a user setup option.
  • api.auth: bearer_token protects /memory/* and /mcp/* with Authorization: Bearer <token>.
  • api.auth: oauth_resource_server exposes MCP protected resource metadata.
  • Protected MCP responses include proper Bearer challenges.
  • OAuth mode validates token audience/resource before processing MCP requests.
  • OAuth mode uses 401 for missing/invalid tokens and 403 for 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.