Skip to main content
Glama
Muhammad-Usman-Tariq

Central Authentication Server for MCP

README.md
# Central Authentication Server for MCP (Model Context Protocol)

A lightweight, production-ready Central Authentication Server built specifically for Model Context Protocol (MCP) ecosystems, migrated to **Python + FastAPI** and backed by **Supabase (PostgreSQL)**.

---

## Migration Overview: What Changed & What Did NOT Change

### What Changed:
- **Backend Framework**: Migrated from Node.js (Express) to **Python 3.12+ (FastAPI + Pydantic + Uvicorn)**.
- **Database Engine**: Migrated from single-file SQLite to **Supabase (PostgreSQL)** using the official `supabase-py` client.
- **Row Level Security (RLS)**: RLS is enabled on all tables (`mcp_clients`, `token_events`, `auth_codes`, `revoked_tokens`, `admin_users`), completely blocking public `anon` access while allowing privileged backend operations through the Supabase `service_role` key.
- **Offline / Local Fallback**: The database layer seamlessly supports both live Supabase and local/offline fallback, ensuring automated tests and development work out-of-the-box.

### What Did NOT Change (100% Contract Preservation):
- **Zero Route / Contract Changes**: All endpoint paths (`/.well-known/...`, `/register`, `/authorize`, `/token`, `/introspect`, `/revocations`, `/admin/...`) and HTTP methods remain identical.
- **Identical JSON Shapes & Status Codes**: Request and response payloads are 100% identical. No MCP server or LLM client configuration requires changes.
- **RS256 JWT Token Structure**: Tokens use the exact same claims (`iss`, `sub`, `aud`, `client_id`, `auth_mode`, `jti`, `iat`, `exp`) and deterministic `kid` signing.
- **Dual Auth Modes**:
  - **Mode 1**: Full OAuth 2.1 flow (RFC 8414 Discovery, RFC 7591 Dynamic Client Registration, mandatory SHA-256 PKCE authorization code exchange, client credentials).
  - **Mode 2**: Static long-lived API key fallback for header-based LLM configs (`headers: { "x-api-key": "..." }`).
- **Admin Control Center UI**: The responsive dark glassmorphic dashboard in `public/admin/` operates identically against the FastAPI backend.
- **Verification Middleware**: The drop-in MCP verification middleware in Python (`sdk/python/mcp_auth_middleware.py`) and Node.js (`sdk/node/mcp-auth-middleware.js`) remain 100% drop-in compatible.

---

## Architecture Diagram

```
                   +-------------------------------------------------------------+
                   |             CENTRAL AUTH SERVER (FastAPI + Python)          |
                   |                                                             |
                   |  [Mode 1: OAuth 2.1]          [Discovery & Keys]            |
                   |  - /.well-known/oauth-...     - /.well-known/jwks.json      |
                   |  - /register (RFC 7591)       - /revocations /introspect    |
                   |  - /authorize (PKCE S256)                                   |
                   |  - /token (auth_code & m2m)   [Admin Dashboard]             |
                   |                               - MCP Client Management       |
                   |  [Key Management & Crypto]    - Static Token Generation     |
                   |  - RS256 Private Key Signing  - Audit Logging               |
                   |  - bcrypt Secret Hashing      - Supabase (Postgres with RLS)|
                   +-------------------------------------------------------------+
                                       ▲                            ▲
                                       │ (1. Auth / Token)          │ (2. Fetch JWKS /
                                       │                            │     Revocations)
                   +-------------------+------+          +----------+-------------------+
                   |     LLM CLIENTS          |          |     ANY MCP SERVER           |
                   |                          |          |                              |
                   | Claude Desktop / Cursor  |          | Drops in 1 Middleware:       |
                   | ChatGPT / Gemini / Agents|  Token   | McpAuthMiddleware(           |
                   | Mode 1: OAuth 2.1 (PKCE) | =======> |   jwks_uri=".../jwks.json",  |
                   | Mode 2: Static Token     | (Header) |   audience="mcp-invoicing"   |
                   | (Both emit RS256 JWT)    |          | )                            |
                   +--------------------------+          | -> ZERO LLM-SPECIFIC CODE!   |
                                                         +------------------------------+
```

---

## Supabase Setup Guide

### 1. Create a Supabase Project
1. Log in to [Supabase](https://supabase.com) and create a new project.
2. Under **Project Settings -> API**, copy:
   - **Project URL**: e.g., `https://xyzcompany.supabase.co`
   - **Service Role Key** (`secret`): `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` *(never expose this key in client-side code)*.

### 2. Execute SQL Schema
Navigate to the **SQL Editor** in your Supabase dashboard and run the contents of [`supabase/schema.sql`](file:///c:/Users/DELL/Desktop/Centeral_auth_MCP/supabase/schema.sql):
- Creates `mcp_clients`, `token_events`, `auth_codes`, `revoked_tokens`, and `admin_users`.
- Configures indexes and unique constraints.
- Enables Row Level Security (RLS) on all tables and revokes direct table access from `anon`.

### 3. Configure Environment Variables
Update `.env` with your Supabase credentials:
```env
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

### 4. Database Migrations for Existing Deployments
When pulling repository updates on an existing deployment, execute any pending incremental migration files in [`supabase/migrations/`](supabase/migrations/) in the Supabase SQL Editor:
- **`001_add_current_static_token_jti.sql`**: Adds `current_static_token_jti` to `mcp_clients` for individual static token rotation tracking and invalidation.

> [!IMPORTANT]
> Always apply pending migrations in `supabase/migrations/` when updating a production Central Auth deployment to ensure the database schema remains synchronized with backend query models.

### 5. (Optional) Migrate Existing SQLite Data
If you had an existing SQLite database at `data/auth.db`, import all existing records directly into Supabase:
```bash
python scripts/migrate_sqlite_to_supabase.py
```

---

## Environment Variables Reference

| Variable | Default | Description | Status |
|---|---|---|---|
| `SUPABASE_URL` | `None` | Supabase project URL (`https://xyz.supabase.co`) | **Added** |
| `SUPABASE_SERVICE_ROLE_KEY` | `None` | Supabase service role key with RLS bypass | **Added** |
| `SUPABASE_KEY` | `None` | Alias fallback for service role key | **Added** |
| `PORT` | `3000` | Port for FastAPI / Uvicorn server | Retained |
| `ISSUER_URL` | `http://localhost:3000` | Public URL for token issuance and discovery | Retained |
| `REQUIRE_HTTPS` | `false` | Enforce HTTPS across all endpoints | Retained |
| `DB_PATH` | `./data/auth.db` | SQLite migration source / offline fallback | Retained |
| `KEYS_DIR` | `./.keys` | Directory storing generated RSA keypair | Retained |
| `PRIVATE_KEY_PEM` | `None` | Optional RSA private key in PEM format | Retained |
| `PUBLIC_KEY_PEM` | `None` | Optional RSA public key in PEM format | Retained |
| `ADMIN_USERNAME` | `admin` | Initial admin username | Retained |
| `ADMIN_PASSWORD` | `admin-mcp-secret-2026` | Initial admin password | Retained |
| `ADMIN_JWT_SECRET` | `central-mcp-...` | Secret used to sign admin session tokens | Retained |
| `ACCESS_TOKEN_EXPIRY` | `3600` | Mode 1 OAuth access token lifespan (seconds) | Retained |
| `STATIC_TOKEN_EXPIRY_DAYS`| `365` | Mode 2 static token lifespan (days) | Retained |
| `AUTH_CODE_EXPIRY_SECONDS`| `300` | PKCE authorization code lifespan (seconds) | Retained |

---

## Adding a new MCP Server (Developer Workflow)

> [!NOTE]
> **Database Schema Prerequisite**: Ensure all schema migrations from `supabase/migrations/` (such as `001_add_current_static_token_jti.sql`) have been applied to your database so client listing, token rotation, and deletion operate seamlessly.

### Step 1: Register MCP in the Admin Console
1. Open `http://localhost:3000/admin`.
2. Click **"Register New MCP"**.
3. Provide the Server Name (e.g. `Invoicing Service`) and Audience (e.g. `mcp-invoicing`).
4. Copy the auto-generated **Mode 2 Static Token** or note the client credentials for Mode 1.

---

### Step 2: Drop Middleware into Your MCP Server

#### In Python (FastAPI / Starlette)
Copy [`sdk/python/mcp_auth_middleware.py`](file:///c:/Users/DELL/Desktop/Centeral_auth_MCP/sdk/python/mcp_auth_middleware.py) into your project:

```python
from fastapi import FastAPI, Request
from mcp_auth_middleware import McpAuthMiddleware

app = FastAPI()

# -------------------------------------------------------------
# THE ONLY AUTH CODE YOU EVER WRITE IN ANY MCP SERVER:
# -------------------------------------------------------------
app.add_middleware(
    McpAuthMiddleware,
    jwks_uri="http://localhost:3000/.well-known/jwks.json",
    audience="mcp-invoicing"
)
# -------------------------------------------------------------

@app.post("/tools/list")
async def list_tools(request: Request):
    # request.state.auth contains verified JWT claims
    # request.state.mcp_client_id contains client identifier
    return {"tools": [{"name": "generate_invoice"}]}
```

#### In Node.js (Express)
Copy [`sdk/node/mcp-auth-middleware.js`](file:///c:/Users/DELL/Desktop/Centeral_auth_MCP/sdk/node/mcp-auth-middleware.js) into your project (*zero external dependencies*):

```javascript
const express = require('express');
const { createMcpAuthMiddleware } = require('./mcp-auth-middleware');

const app = express();
app.use(express.json());

app.use(createMcpAuthMiddleware({
  jwksUri: 'http://localhost:3000/.well-known/jwks.json',
  audience: 'mcp-invoicing'
}));

app.post('/tools/list', (req, res) => {
  res.json({ tools: [{ name: 'generate_invoice' }] });
});
```

---

## Migration Verification Checklist

The migrated system was tested against the exact same test cases as the original server:

| Endpoint / Feature | Method | Status | Verification Detail |
|---|---|---|---|
| **Server Metadata** | `GET /.well-known/oauth-authorization-server` | Verified | RFC 8414 metadata, mandates `S256` PKCE |
| **Resource Metadata** | `GET /.well-known/oauth-protected-resource` | Verified | Protected Resource Metadata (PRM) response |
| **JWKS Endpoint** | `GET /.well-known/jwks.json` | Verified | RFC 7517 public keys in RS256 format |
| **Admin Login** | `POST /admin/login` | Verified | Bcrypt verification, issues Admin JWT session |
| **Admin Stats** | `GET /admin/api/stats` | Verified | Total clients, active clients, event count |
| **Create MCP Client** | `POST /admin/api/clients` | Verified | Client ID & Secret (shown ONCE) + Mode 2 Token |
| **Dynamic Registration**| `POST /register` | Verified | RFC 7591 DCR, returns 201 with client metadata |
| **OAuth 2.1 Authorize**| `GET /authorize` | Verified | Enforces PKCE S256, redirects with 302 and code |
| **Token Exchange (PKCE)**| `POST /token` | Verified | Validates `code_verifier`, issues RS256 Bearer token |
| **PKCE Verifier Check** | `POST /token` | Verified | Rejects invalid code verifier (400 Bad Request) |
| **Code Replay Protection**| `POST /token` | Verified | Rejects reused authorization code (400 Bad Request) |
| **Client Credentials** | `POST /token` | Verified | Machine-to-machine M2M RS256 token issuance |
| **Token Introspection** | `POST /introspect` | Verified | RFC 7662 returns active: true/false |
| **Revocation Check** | `GET /revocations` | Verified | Returns live list of revoked client IDs |
| **Admin Revoke** | `POST /admin/api/clients/{id}/revoke` | Verified | Immediately invalidates Mode 1 and Mode 2 tokens |
| **Static Token Gen** | `POST /admin/api/clients/{id}/static-token` | Verified | Issues fresh 90-day RS256 JWT |
| **Audit Logging** | `GET /admin/api/audit` | Verified | Records all issuances, revocations, and failures |
| **Middleware (Python)**| ASGI Dispatch | Verified | Validates JWKS signature, aud, exp, revocation |
| **Middleware (Node)** | Express Handler | Verified | Compatible with tokens from migrated server |

---

## Running the Application

### 1. Set up Virtual Environment
```bash
uv venv .venv
uv pip install -r requirements.txt
```

### 2. Run Automated Test Suite
```bash
.venv\Scripts\pytest tests/test_auth.py
# or
.venv\Scripts\python tests/test_auth.py
```
*Executes all 8 test suites validating 100% feature parity with 100% pass rate.*

### 3. Start the FastAPI Central Auth Server
```bash
.venv\Scripts\uvicorn src_py.main:app --port 3000 --reload
```
Open `http://localhost:3000/admin` to access the Control Center.

---

## Docker Deployment

The Central Auth Server includes a multi-stage production Dockerfile (`python:3.12-slim`) and a `docker-compose.yml` for containerized hosting.

### 1. Environment Configuration
Create your `.env` file from the example template:
```bash
cp .env.example .env
```
Ensure the following variables are configured:
- `SUPABASE_URL` & `SUPABASE_SERVICE_ROLE_KEY`: Your Supabase project credentials.
- `ADMIN_PASSWORD` & `ADMIN_JWT_SECRET`: Strong secret credentials for operator console sessions.
- `REQUIRE_HTTPS=true`: Mandatory for production deployments.
- `PORT=8000`: Container application port.
- `WEB_CONCURRENCY=4`: Number of uvicorn worker processes (defaults to `2`).

### 2. Run with Docker Compose (Recommended)
`docker-compose.yml` automatically mounts a persistent named volume for the `.keys/` directory.

> [!IMPORTANT]
> **RSA Keypair Volume Persistence**: The `.keys/` directory holds the 2048-bit RSA private and public keypair. Persisting this directory via the `mcp_keys` Docker volume ensures that your server's keypair remains constant across container restarts and redeployments. Regenerating keys on restart would immediately invalidate all issued 90-day static tokens and active JWTs.

Start the service in the background:
```bash
docker compose up -d --build
```
Check health and logs:
```bash
docker compose ps
docker compose logs -f central-auth-mcp
```
To stop the service:
```bash
docker compose down
```

### 3. Build & Run Manually with Docker CLI
If you prefer running without compose:
```bash
# 1. Create a dedicated named volume for RSA keys
docker volume create central_auth_keys

# 2. Build the production image
docker build -t central-auth-mcp:latest .

# 3. Run container as non-root user with persistent volume
docker run -d \
  --name central-auth-mcp \
  --restart unless-stopped \
  -p 8000:8000 \
  --env-file .env \
  -v central_auth_keys:/app/.keys \
  central-auth-mcp:latest
```

---

## Production VPS Deployment (non-Docker)

For standard Linux VPS hosting (Ubuntu / Debian / RHEL), run the server as a system service managed by `systemd` to provide automatic recovery on crash or reboot.

### 1. Install & Configure Application
```bash
# 1. Create a dedicated system user
sudo useradd -r -s /bin/false -d /opt/central-auth-mcp mcpuser

# 2. Clone and set up repository in /opt
sudo git clone <REPO_URL> /opt/central-auth-mcp
cd /opt/central-auth-mcp

# 3. Create virtual environment and install dependencies
sudo python3 -m venv .venv
sudo /opt/central-auth-mcp/.venv/bin/pip install --no-cache-dir -r requirements.txt

# 4. Configure production environment
sudo cp .env.example .env
sudo nano .env # Set REQUIRE_HTTPS=true, Supabase credentials, strong passwords

# 5. Set directory ownership and key permissions
sudo chown -R mcpuser:mcpuser /opt/central-auth-mcp
sudo chmod 700 /opt/central-auth-mcp/.keys
```

### 2. Systemd Service Unit File
Create `/etc/systemd/system/central-auth-mcp.service`:
```ini
[Unit]
Description=Central Authentication Server for MCP (FastAPI + Supabase)
After=network.target

[Service]
Type=simple
User=mcpuser
Group=mcpuser
WorkingDirectory=/opt/central-auth-mcp
EnvironmentFile=/opt/central-auth-mcp/.env
ExecStart=/opt/central-auth-mcp/.venv/bin/uvicorn src_py.main:app --host 127.0.0.1 --port 8000 --workers 4
Restart=always
RestartSec=3s
KillMode=process

# Security sandboxing
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
```

### 3. Enable and Start Service
```bash
sudo systemctl daemon-reload
sudo systemctl enable central-auth-mcp
sudo systemctl start central-auth-mcp

# Verify service status
sudo systemctl status central-auth-mcp
```

---

## Reverse Proxy & HTTPS

In production, terminate TLS using an Nginx reverse proxy running on the host. Nginx handles SSL/TLS termination and proxies HTTP requests to Uvicorn on `127.0.0.1:8000`.

### 1. Nginx Configuration
Create `/etc/nginx/sites-available/central-auth-mcp`:

```nginx
# HTTP - Redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name auth.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS - Terminate TLS and proxy to FastAPI Uvicorn
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name auth.example.com;

    # SSL Certificates (managed by Certbot)
    ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;

    # Proxy to Central Auth Uvicorn process
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;

        # CRITICAL HEADERS:
        # The central auth server reads X-Forwarded-Proto to enforce HTTPS
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;

        proxy_connect_timeout 10s;
        proxy_read_timeout 60s;
    }
}
```

Enable site configuration:
```bash
sudo ln -s /etc/nginx/sites-available/central-auth-mcp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```

### 2. Obtain Free SSL Certificate via Let's Encrypt (Certbot)
```bash
sudo apt update
sudo apt install -y certbot python3-certbot-nginx

# Obtain certificate and let Certbot configure Nginx automatically
sudo certbot --nginx -d auth.example.com

# Verify auto-renewal timer
sudo systemctl status certbot.timer
sudo certbot renew --dry-run
```

---

## OAuth 2.1 Flow — Verified

The full OAuth 2.1 PKCE authorization code flow has been verified end-to-end against the live server. You can mirror the automated test manually using `curl`:

### Step 1: Dynamic Client Registration (RFC 7591)
Register your client dynamically to receive a `client_id` and `client_secret`:
```bash
curl -X POST https://auth.example.com/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Claude Desktop Agent",
    "redirect_uris": ["http://localhost:8080/callback"],
    "audience": "mcp-filesystem"
  }'
```
*Output:*
```json
{
  "client_id": "mcp_claude-desktop_a1b2c3d4",
  "client_secret": "mcp_sec_...",
  "audience": "mcp-filesystem",
  "redirect_uris": ["http://localhost:8080/callback"],
  "grant_types": ["authorization_code", "client_credentials"]
}
```

### Step 2: Generate PKCE S256 Code Verifier & Challenge
In Bash, generate a random 43-128 character verifier and its S256 SHA-256 base64url challenge:
```bash
# Generate 64-byte random verifier
VERIFIER=$(openssl rand -base64 48 | tr -d '=+/' | cut -c1-64)

# Compute S256 challenge: Base64URL(SHA256(verifier)) without padding
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -e | tr '+/' '-_' | tr -d '=')

echo "Verifier:  $VERIFIER"
echo "Challenge: $CHALLENGE"
```

### Step 3: Authorization Request (`/authorize`)
Send the user or agent browser to the authorization endpoint:
```bash
curl -i -G "https://auth.example.com/authorize" \
  --data-urlencode "response_type=code" \
  --data-urlencode "client_id=mcp_claude-desktop_a1b2c3d4" \
  --data-urlencode "redirect_uri=http://localhost:8080/callback" \
  --data-urlencode "code_challenge=$CHALLENGE" \
  --data-urlencode "code_challenge_method=S256" \
  --data-urlencode "state=secure_random_state_123"
```
*Response:* Returns HTTP `302 Found` with redirect target containing the single-use authorization code:
```http
HTTP/2 302
Location: http://localhost:8080/callback?code=ac_8f9a2b4c6e1d&state=secure_random_state_123
```

### Step 4: Token Exchange (`/token`)
Exchange the authorization code for an RS256 Bearer JWT by providing the original `code_verifier`:
```bash
curl -X POST https://auth.example.com/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "ac_8f9a2b4c6e1d",
    "redirect_uri": "http://localhost:8080/callback",
    "code_verifier": "'"$VERIFIER"'",
    "client_id": "mcp_claude-desktop_a1b2c3d4",
    "client_secret": "mcp_sec_..."
  }'
```
*Response:*
```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im1jcC1hdXRoLTIwMjYtMDEiLCJ0eXAiOiJKV1QifQ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "mcp:all"
}
```

### Step 5: Verify Token Signature Locally via JWKS
Any MCP server can immediately verify the token offline using the public RS256 key:
```bash
curl https://auth.example.com/.well-known/jwks.json
```

---

## Production Checklist

Before exposing the Central Auth Server to production traffic, verify each item:

- [ ] **`REQUIRE_HTTPS=true` in `.env`**: Enforces HTTPS and rejects unencrypted connections.
- [ ] **`ADMIN_PASSWORD` updated**: Changed from `admin-mcp-secret-2026` to a high-entropy passphrase.
- [ ] **`ADMIN_JWT_SECRET` updated**: Changed from default string to a 32+ character random secret.
- [ ] **`.keys/` directory backed up & persisted**: Mounted to a persistent Docker named volume or stored outside disposable deploy paths on VPS.
- [ ] **Supabase `service_role` key protected**: Stored strictly in server `.env`, never committed to Git, never exposed to clients.
- [ ] **Rate limiting configured**: Verified `RATE_LIMIT_WINDOW_MS` and `RATE_LIMIT_MAX_REQUESTS` match expected traffic capacity.
- [ ] **Auto-restart confirmed**: `systemd` service (`Restart=always`) or Docker container restart policy (`restart: unless-stopped`) active and tested across system reboots.
- [ ] **Healthcheck monitored**: `/health` endpoint responding with `{"status": "ok"}` and integrated into external uptime monitors.