How to Configure OAuth Authentication with Dynamic Client Registration in MCP Memory Service

Enable OAuth 2.1 in MCP Memory Service by setting MCP_OAUTH_ENABLED=true, configure your storage backend via environment variables, and use the RFC 7591-compliant POST /oauth/register endpoint to dynamically register clients with automatic credential generation.

The MCP Memory Service repository (doobidoo/mcp-memory-service) ships with a complete, production-ready OAuth 2.1 implementation that supports dynamic client registration according to RFC 7591. This optional security layer activates only when explicitly enabled through environment configuration, providing automated client provisioning, JWT token issuance, and Bearer token validation without requiring manual client credential management.

Prerequisites and Environment Configuration

All OAuth behavior originates in src/mcp_memory_service/config.py, which reads environment variables at startup to construct the issuer URL, generate cryptographic keys, and select storage backends.

Set these required variables to activate the system:

export MCP_OAUTH_ENABLED=true
export MCP_OAUTH_ISSUER=https://api.example.com
export MCP_OAUTH_STORAGE_BACKEND=sqlite
export MCP_OAUTH_SQLITE_PATH=./data/oauth.db

The service automatically generates an RSA key-pair on startup if MCP_OAUTH_PRIVATE_KEY and MCP_OAUTH_PUBLIC_KEY are omitted (see lines 49-84 in config.py). For symmetric signing only, omit the RSA variables and the system will fall back to HS256 using a generated symmetric secret.

OAuth 2.1 Architecture Overview

The implementation spans seven core components that handle discovery, registration, storage, and request validation:

  • oauth/discovery.py: Exposes .well-known endpoints (/oauth-authorization-server/mcp and /openid-configuration/mcp) returning OAuthServerMetadata containing the registration URL
  • oauth/registration.py: Implements POST /oauth/register for dynamic client registration, validating ClientRegistrationRequest payloads and returning ClientRegistrationResponse with generated credentials
  • oauth/models.py: Defines Pydantic schemas for registration requests, server metadata, and the internal RegisteredClient representation used by storage layers
  • oauth/storage/factory.py: Creates storage backends based on MCP_OAUTH_STORAGE_BACKEND (accepts "memory" or "sqlite")
  • oauth/storage/memory.py: In-memory dev/testing backend with volatile storage
  • oauth/storage/sqlite.py: Persistent production backend for registered clients and tokens
  • oauth/middleware.py: FastAPI dependency that validates Authorization: Bearer headers, supporting both JWT and stored access tokens with fallback to API-key authentication

These components are wired into the FastAPI application in src/mcp_memory_service/web/app.py, which mounts the /oauth/* routers and integrates the authentication middleware into the request pipeline.

Step-by-Step Configuration

Enable OAuth via Environment Variables

The config.py module exposes helper functions including get_jwt_algorithm(), get_jwt_signing_key(), and get_jwt_verification_key() that depend on your environment configuration. When MCP_OAUTH_ENABLED is true, the system derives the OAUTH_ISSUER from the server host/port unless overridden by MCP_OAUTH_ISSUER (lines 121-131).

For reverse-proxy deployments, explicitly set the issuer to your external URL:

export MCP_OAUTH_ISSUER=https://api.example.com
export MCP_OAUTH_PRIVATE_KEY="$(cat private.pem)"
export MCP_OAUTH_PUBLIC_KEY="$(cat public.pem)"

Configure Storage Backends

The storage factory in oauth/storage/factory.py (lines 31-70) instantiates the appropriate backend based on the MCP_OAUTH_STORAGE_BACKEND variable:

  • memory: Non-persistent storage suitable for development and testing
  • sqlite: File-based persistence for production environments

The SQLite backend stores RegisteredClient objects including client_id, client_secret, redirect URIs, and grant types, while the memory backend maintains identical behavior but loses data on process termination.

Set Up Discovery Endpoints

Before registering clients, verify the discovery endpoints are accessible. The discovery.py module (lines 31-56) exposes metadata at:


GET /.well-known/oauth-authorization-server/mcp
GET /.well-known/openid-configuration/mcp

These return JSON containing the issuer, authorization_endpoint, token_endpoint, and registration_endpoint URLs required by OAuth 2.1 clients.

Dynamic Client Registration Workflow

The Registration Endpoint

The dynamic registration implementation lives in oauth/registration.py (lines 1-55, with validation logic at lines 45-99). Clients submit a ClientRegistrationRequest to POST /oauth/register with their metadata:

curl -X POST https://api.example.com/oauth/register \
     -H "Content-Type: application/json" \
     -d '{
           "redirect_uris": ["https://myapp.example.com/callback"],
           "grant_types": ["authorization_code"],
           "response_types": ["code"],
           "client_name": "My Application",
           "token_endpoint_auth_method": "client_secret_basic"
         }'

Validating Client Metadata

The registration endpoint validates redirect URIs via validate_redirect_uris, checks supported grant types and response types, and ensures the requested authentication method is supported. Upon validation, it calls get_oauth_storage().generate_client_id() and generate_client_secret() to create unique credentials.

Storage and Persistence

The storage backend persists a RegisteredClient model (defined in models.py lines 70-83) containing the generated credentials and client metadata. For SQLite backends, this creates a durable record in oauth.db; for memory backends, the client exists only for the process lifetime.

The endpoint returns a ClientRegistrationResponse JSON object:

{
  "client_id": "c2a1f9e7-5c3b-4a12-9f7b-2c1e4d5f6a78",
  "client_secret": "a1b2c3d4e5f6...",
  "redirect_uris": ["https://myapp.example.com/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "client_secret_basic",
  "client_name": "My Application"
}

Authenticating Requests

Token Validation Middleware

Once registered, clients obtain access tokens (handled in authorization.py and token.py elsewhere in the codebase) and present them via the Authorization: Bearer <token> header.

The oauth/middleware.py dependency (lines 1-45, with validation at lines 83-132) processes each request:

  1. Extracts the Bearer token from the header
  2. Attempts JWT validation using validate_jwt_token with issuer, audience, and claim verification
  3. Falls back to storage lookup for stored access tokens (e.g., refresh token flows)
  4. Returns an AuthenticationResult object with client_id, scope, and authentication method

If validation fails, the middleware returns a 401 error with an RFC 6750-compatible error document. If ALLOW_ANONYMOUS_ACCESS is configured, unauthenticated requests may proceed with limited privileges.

Practical Implementation Examples

Complete Environment Setup

Configure a production-ready instance with SQLite persistence and external issuer:


# .env file

MCP_OAUTH_ENABLED=true
MCP_OAUTH_STORAGE_BACKEND=sqlite
MCP_OAUTH_SQLITE_PATH=./data/oauth.db
MCP_OAUTH_ISSUER=https://api.example.com
MCP_OAUTH_PRIVATE_KEY="$(cat /secrets/oauth-private.pem)"
MCP_OAUTH_PUBLIC_KEY="$(cat /secrets/oauth-public.pem)"

Dynamic Client Registration

Register a new client and capture the credentials:

RESPONSE=$(curl -s -X POST https://api.example.com/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uris": ["https://app.example.com/oauth/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "client_name": "Production App",
    "scope": "memory:read memory:write"
  }')

CLIENT_ID=$(echo $RESPONSE | jq -r '.client_id')
CLIENT_SECRET=$(echo $RESPONSE | jq -r '.client_secret')

Accessing Protected Endpoints

Use the registered credentials to obtain a token (via the authorization flow or token endpoint), then access MCP endpoints:

curl https://api.example.com/memory/v1/items \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     -H "Content-Type: application/json"

Direct Storage Backend Access

For debugging or administrative operations, interact with the storage layer directly:

from mcp_memory_service.web.oauth.storage.factory import create_oauth_storage
import asyncio

async def audit_clients():
    storage = create_oauth_storage(
        backend_type="sqlite", 
        db_path="./data/oauth.db"
    )
    clients = await storage.list_clients()
    for client in clients:
        print(f"Client: {client.client_id} - {client.client_name}")

asyncio.run(audit_clients())

This imports the factory function from oauth/storage/factory.py and creates a storage instance matching your configuration.

Summary

  • Enable OAuth by setting MCP_OAUTH_ENABLED=true and configuring MCP_OAUTH_ISSUER in your environment
  • Choose storage via MCP_OAUTH_STORAGE_BACKEND (sqlite for production, memory for testing) as implemented in storage/factory.py
  • Register clients dynamically using POST /oauth/register which validates ClientRegistrationRequest and returns client_id/client_secret via the registration.py endpoint
  • Authenticate requests using Bearer tokens validated by the middleware in oauth/middleware.py, which supports JWT (RS256/HS256) and stored token lookup
  • Configure keys manually via environment variables or allow automatic RSA key generation in config.py lines 49-84

Frequently Asked Questions

What OAuth 2.1 flows does MCP Memory Service support?

According to the oauth/registration.py source code, the service validates authorization_code and refresh_token grant types through the ClientRegistrationRequest model. The token endpoint (implemented separately in authorization.py and token.py) issues JWT access tokens signed with RS256 (when RSA keys are configured) or HS256 (fallback symmetric signing), supporting the standard authorization code flow with PKCE and refresh token rotation.

How does dynamic client registration differ from static client configuration?

Dynamic client registration (RFC 7591) allows OAuth clients to self-register at runtime via POST /oauth/register without manual intervention. The registration.py endpoint automatically generates client_id and client_secret through the storage backend's generate_client_id() and generate_client_secret() methods, persists the RegisteredClient model, and returns credentials immediately. Static configuration would require pre-provisioning these values in a database or configuration file before deployment.

Can I use different storage backends for OAuth clients and memory data?

Yes. The OAuth storage system is decoupled from the main memory service storage. Set MCP_OAUTH_STORAGE_BACKEND independently to choose between memory (volatile, dev-only) or sqlite (persistent). The factory in oauth/storage/factory.py creates isolated instances, so you can run OAuth with SQLite persistence while keeping memory data in a different backend. For production, always use sqlite to prevent losing registered client credentials on restart.

How do I troubleshoot authentication failures in the middleware?

Check the oauth/middleware.py validation logic (lines 83-132). The middleware first attempts validate_jwt_token which verifies the JWT signature against get_jwt_verification_key(), checks the issuer matches OAUTH_ISSUER, and validates required claims. If JWT validation fails, it falls back to looking up the token as a stored access token in your configured backend. Enable detailed logging to see whether failures occur during signature verification (indicating key mismatches) or during storage lookup (indicating expired or unknown tokens).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →