McpAuth is the authentication and authorization component of the MCP Gateway Proof of Concept (PoC) described in the paper:
Simplified and Secure MCP Gateways for Enterprise AI Integration
Ivo Brett, CISSP, B.Eng, MSc
View Paper (2025)
This repository is part of a broader initiative to enable secure, scalable, and compliant enterprise integration with the Model Context Protocol (MCP). See the website SelfHostedMCP.com. It provides an extensible OAuth2.1-based authentication gateway that offloads identity, authorization, and policy management from backend MCP servers—ensuring conformance with the 2025-03-26 MCP Specification.
---McpAuth is designed to:
- Decouple security logic from MCP servers
- Centralize identity management using OAuth 2.1 & OIDC
- Support dynamic client registration
- Enforce fine-grained token scopes and policy controls
- Act as a composable module in enterprise-grade Zero Trust architectures
This implementation is part of a larger PoC that validates:
- A reference MCP Gateway architecture for secure deployments
- Threat model mapping aligned with frameworks such as MAESTRO and Narajala & Habler
- Real-world compatibility with tools like Cloudflare Tunnels, WireGuard, Traefik, and CrowdSec
The full proof of concept includes:
- Two isolated MCP servers (local and cloud-based)
- Secure tunneling via WireGuard and Pangolin
- Centralized intrusion detection and observability
- Seamless integration with Anthropic's MCP Inspector
- OAuth2 authentication with PKCE via Traefik
forwardAuth - Seamless integration with MCP Gateway SSE endpoints
- Email whitelisting for controlled access
- Docker-ready, easy to deploy
- Includes a Python-based test server
Go to the Google Cloud Console Navigate to APIs & Services > Credentials Click Create Credentials → OAuth client ID Choose Web Application Add an Authorized redirect URI — you’ll get this later when you set up Traefik, but it will look like: https://oauth.yourdomain.com/callback
Save the Client ID and Client Secret for later use.
CLIENT_ID=<INSERT_VALUE_FROM_GOOGLE>
CLIENT_SECRET=<INSERT_VALUE_FROM_GOOGLE>
Use flags or environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
11000 |
Port for the auth server |
PROTECTED_PATH |
/sse |
Protected endpoint path |
OAUTH_DOMAIN |
(none) | OAuth issuer domain |
CLIENT_ID |
(none) | OAuth client ID |
CLIENT_SECRET |
(none) | OAuth client secret |
ALLOWED_EMAILS |
(none) | Comma-separated list of allowed emails |
LOG_LEVEL |
1 |
0=debug, 1=info, 2=minimal |
MCPAuth supports fine-grained scope control to enhance security by limiting token privileges. You can define which scopes are allowed in an OAuth request and which are required for a token to be considered valid.
- Allowed Scopes: A whitelist of scopes that the middleware is permitted to request from the OAuth provider. If a client requests scopes not in this list, they will be ignored.
- Required Scopes: A list of scopes that must be present in the granted token after the user authenticates. If the token does not contain all of these scopes, access will be denied with a
403 Forbidden (Insufficient Scope)error.
This allows you to enforce policies like requiring an email scope for all users while allowing clients to optionally request additional permissions like profile or custom API scopes.
You can configure scopes using command-line flags or environment variables:
| Variable | Flag | Description |
|---|---|---|
ALLOWED_SCOPES |
-allowedScopes |
Comma-separated list of allowed OAuth scopes. |
REQUIRED_SCOPES |
-requiredScopes |
Comma-separated list of required OAuth scopes. |
Example:
To allow clients to request openid, email, and profile scopes, but require that all valid tokens include at least openid and email, you would set:
ALLOWED_SCOPES=openid,email,profileREQUIRED_SCOPES=openid,email
services:
mcpauth:
image: oideibrett/mcpauth:latest
environment:
- PORT=11000
- CLIENT_ID=${CLIENT_ID}
- CLIENT_SECRET=${CLIENT_SECRET}
ports:
- "11000:11000"
traefik:
image: traefik::v3.4.1
command:
- "--providers.docker=true"
- "--entrypoints.websecure.address=:443"
ports:
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock- Go 1.21+
- Traefik v2.x+
- An OAuth provider (e.g., Google, GitHub)
git clone https://github.com/oidebrett/mcpauth
cd mcpauth
go mod tidygo run cmd/main.go -port=11000 -oauthDomain=your-domain.comdocker buildx build --platform linux/amd64,linux/arm64 -t oideibrett/mcpauth:dev --push .services:
mcpauth:
build: .
environment:
- PORT=11000
- CLIENT_ID=${CLIENT_ID}
- CLIENT_SECRET=${CLIENT_SECRET}
ports:
- "11000:11000"http:
middlewares:
mcp-auth:
forwardAuth:
address: "http://mcpauth:11000/auth"
authResponseHeaders:
- "X-Forwarded-User"labels:
- "traefik.http.routers.myapp.middlewares=mcp-auth@file"cd test_mcp_server
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python mcp-server-sse.pyHere are a few curl commands to test different authentication and authorization scenarios.
1. Health Check
This command checks if the mcpauth service is running and responsive. You should receive a 200 OK response.
curl -i http://localhost:11000/health2. Accessing a Protected Endpoint (No Token)
When you try to access a protected endpoint like /sse without a valid token, mcpauth should initiate the OAuth2 authentication flow. For a non-browser client like curl, this will result in a 302 Found redirect to the Google login page.
curl -i http://localhost:11000/sseExpected Output: An HTTP 302 Found redirecting to `https://accounts.google.com/...
3. Accessing a Protected Endpoint (Valid Token)
Once you have a valid bearer token from the OAuth provider, you can use it to access the protected endpoint. This request should be successful (200 OK), and mcpauth will forward the request to the upstream service.
# Replace YOUR_VALID_TOKEN with an actual bearer token
curl -i -H "Authorization: Bearer YOUR_VALID_TOKEN" http://localhost:11000/sseExpected Output: An HTTP 200 OK and the response from the test server (e.g., the SSE stream).
4. Accessing a Protected Endpoint (Invalid/Expired Token)
If you use a token that is invalid, malformed, or expired, mcpauth should deny access. This will likely result in a 401 Unauthorized error, prompting for re-authentication.
curl -i -H "Authorization: Bearer INVALID_TOKEN" http://localhost:11000/sseExpected Output: An HTTP 401 Unauthorized response.
5. Accessing with a Token from an Unauthorized User
If the ALLOWED_EMAILS list is configured, mcpauth will validate the user's email from the token. If the user is not on the whitelist, access will be denied.
# Use a valid token from a user whose email is NOT in ALLOWED_EMAILS
curl -i -H "Authorization: Bearer TOKEN_FROM_UNAUTHORIZED_USER" http://localhost:11000/sseExpected Output: An HTTP 403 Forbidden response.
Apply middlewares in this order:
mcp-cors-headersredirect-regexmcp-auth
Example dynamic config:
http:
middlewares:
mcp-cors-headers:
headers:
accessControlAllowCredentials: true
accessControlAllowHeaders:
- Authorization
- Content-Type
- mcp-protocol-version
accessControlAllowMethods:
- GET
- POST
- OPTIONS
accessControlAllowOriginList:
- "*"
accessControlMaxAge: 86400
addVaryHeader: true
redirect-regex:
redirectRegex:
regex: "^https://([a-z0-9-]+)\.(.+)/\.well-known/(.+)"
replacement: "https://oauth.${2}/.well-known/${3}"
permanent: true
mcp-auth:
forwardAuth:
address: "http://mcpauth:11000/sse"
authResponseHeaders:
- X-Forwarded-UserThis project supports middleware-manager.
Example templates.yml:
middlewares:
- id: mcp-auth
name: MCP Authentication
type: forwardAuth
config:
address: "http://mcpauth:11000/sse"
authResponseHeaders:
- "X-Forwarded-User"
- id: mcp-cors-headers
name: MCP CORS Headers
type: headers
config:
accessControlAllowMethods:
- GET
- POST
- OPTIONS
accessControlAllowOriginList:
- "*"
accessControlAllowHeaders:
- Authorization
- Content-Type
- mcp-protocol-version
accessControlMaxAge: 86400
accessControlAllowCredentials: true
addVaryHeader: true
- id: redirect-regex
name: Regex Redirect
type: redirectregex
config:
regex: "^https://([a-z0-9-]+)\.yourdomain\.com/\.well-known/oauth-authorization-server"
replacement: "https://oauth.yourdomain.com/.well-known/oauth-authorization-server"
permanent: trueLicensed under the GNU General Public License v3.0.