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-knownendpoints (/oauth-authorization-server/mcpand/openid-configuration/mcp) returningOAuthServerMetadatacontaining the registration URLoauth/registration.py: ImplementsPOST /oauth/registerfor dynamic client registration, validatingClientRegistrationRequestpayloads and returningClientRegistrationResponsewith generated credentialsoauth/models.py: Defines Pydantic schemas for registration requests, server metadata, and the internalRegisteredClientrepresentation used by storage layersoauth/storage/factory.py: Creates storage backends based onMCP_OAUTH_STORAGE_BACKEND(accepts"memory"or"sqlite")oauth/storage/memory.py: In-memory dev/testing backend with volatile storageoauth/storage/sqlite.py: Persistent production backend for registered clients and tokensoauth/middleware.py: FastAPI dependency that validatesAuthorization: Bearerheaders, 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 testingsqlite: 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:
- Extracts the Bearer token from the header
- Attempts JWT validation using
validate_jwt_tokenwith issuer, audience, and claim verification - Falls back to storage lookup for stored access tokens (e.g., refresh token flows)
- Returns an
AuthenticationResultobject withclient_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=trueand configuringMCP_OAUTH_ISSUERin your environment - Choose storage via
MCP_OAUTH_STORAGE_BACKEND(sqlitefor production,memoryfor testing) as implemented instorage/factory.py - Register clients dynamically using
POST /oauth/registerwhich validatesClientRegistrationRequestand returnsclient_id/client_secretvia theregistration.pyendpoint - 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.pylines 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →