This MCP server implements proper OAuth 2.0 authentication with JWT token validation. Unlike the previous "accept any token" approach, this implementation provides:
✅ Real JWT token validation with expiration ✅ Client credential verification ✅ Authorization code flow with one-time use codes ✅ Refresh token support ✅ Token revocation capability ✅ Comprehensive audit logging ✅ Automatic token cleanup
Claude.ai → OAuth Discovery → Client Registration → Authorization Code → Token Exchange → JWT Validation → MCP Request
- JWT Tokens: Signed with secret, expire in 24 hours
- Refresh Tokens: Random 32-byte strings, expire in 7 days
- Authorization Codes: One-time use, expire in 5 minutes
- Client Secrets: UUID-based, validated on token exchange
- Audit Logging: Every request logged with client ID and tool name
# Required
TRANSPORT=http
PORT=3017
BASE_URL=https://atem-mcp.cochran.cloud # CRITICAL for OAuth!
# Optional - JWT Secret (auto-generated if not provided)
JWT_SECRET=your-secret-key-here # 64-byte hex recommended# Generate a random 64-byte secret
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"For production: Set JWT_SECRET in environment to persist across restarts.
Claude.ai fetches:
GET /.well-known/oauth-authorization-serverGET /.well-known/oauth-protected-resource/mcp
POST /register
{
"client_name": "Claude",
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"]
}Response includes client_id and client_secret (stored by server).
GET /authorize?client_id=xxx&redirect_uri=xxx&state=xxxServer generates 5-minute authorization code and redirects to Claude.
POST /token
{
"grant_type": "authorization_code",
"code": "xxx",
"client_id": "xxx",
"client_secret": "xxx"
}Server validates:
- ✓ Authorization code exists and matches client
- ✓ Code hasn't expired (5 min)
- ✓ Client credentials are correct
- ✓ Code hasn't been used (one-time use)
Returns JWT access token + refresh token.
POST /mcp
Authorization: Bearer <JWT>Server validates:
- ✓ JWT signature is valid
- ✓ Token hasn't expired
- ✓ Token hasn't been revoked
- ✓ Updates usage statistics
{
"sub": "client-id",
"scope": "mcp:tools",
"iat": 1234567890,
"exp": 1234654290
}Signed with HS256 using JWT_SECRET.
Every MCP request logs:
[AUDIT] 2026-04-08T12:34:56.789Z | client=abc123 | method=tools/call | tool=cut_to_input
Use cases:
- Track who did what and when
- Identify suspicious patterns
- Debug integration issues
- Compliance/forensics
| State | Lifetime | Cleanup |
|---|---|---|
| Authorization Code | 5 minutes | Immediate after use or expiry |
| Access Token (JWT) | 24 hours | Auto-cleanup every 15 min |
| Refresh Token | 7 days | Auto-cleanup every 15 min |
// Revoke single token
tokenManager.revokeToken(accessToken);
// Revoke all tokens for a client
tokenManager.revokeClientTokens(clientId);When to revoke:
- User logs out
- Security incident detected
- Client deactivated
- Suspicious activity
Add Cloudflare Access in front of your tunnel for:
- ✅ SSO authentication (Google, GitHub, etc.)
- ✅ IP whitelisting
- ✅ Device posture checks
- ✅ Session recording
Setup:
- Go to Cloudflare Zero Trust dashboard
- Create Access Policy for
atem-mcp.cochran.cloud - Add allowed users/groups
- Enable "Require WARP" for extra security
# Cloudflare Tunnel (current)
cloudflared tunnel --config ~/.cloudflared/atem-mcp-3017-config.yml run
# Firewall rules (recommended)
sudo ufw allow from 192.168.0.0/16 to any port 3017 # LAN only
sudo ufw deny 3017 # Block public access to raw portBefore:
// Accept ANY bearer token (don't validate — just log)
console.error(`[AUTH] ✓ Bearer ${token.substring(0, 8)}... known=${validTokens.has(token)}`);After:
// Validate JWT token
const validation = tokenManager.validateToken(token);
if (!validation.valid) {
res.status(401).json({ error: 'Invalid or expired token' });
return;
}❌ Random Bearer tokens no longer work ✅ Only valid JWTs issued by this server work
Claude.ai: No changes needed - OAuth flow handles everything Custom clients: Must complete OAuth flow to get valid tokens
curl https://atem-mcp.cochran.cloud/healthconst stats = tokenManager.getStats();
console.log(stats);
// { activeTokens: 5, activeClients: 3, totalRequests: 142 }[AUTH] ✓ Valid token for <client> (used X times) # Good
[AUTH] ✗ Invalid token signature # Attack attempt
[AUTH] ✗ Token expired # Normal (refresh needed)
[AUTH] ✗ Unknown client # Registration needed
[AUDIT] <timestamp> | client=<id> | tool=<name> # Every action
# 1. Get discovery endpoints
curl https://atem-mcp.cochran.cloud/.well-known/oauth-authorization-server | jq
# 2. Register client
curl -X POST https://atem-mcp.cochran.cloud/register \
-H "Content-Type: application/json" \
-d '{"client_name":"test"}' | jq
# 3. Get authorization code (use browser)
https://atem-mcp.cochran.cloud/authorize?client_id=<id>&redirect_uri=http://localhost
# 4. Exchange code for token
curl -X POST https://atem-mcp.cochran.cloud/token \
-H "Content-Type: application/json" \
-d '{
"grant_type":"authorization_code",
"code":"<code>",
"client_id":"<id>",
"client_secret":"<secret>"
}' | jq
# 5. Test MCP endpoint
curl -X POST https://atem-mcp.cochran.cloud/mcp \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | jq# Should FAIL - invalid token
curl -X POST https://atem-mcp.cochran.cloud/mcp \
-H "Authorization: Bearer fake-token" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Should return 401 UnauthorizedA: No! Claude.ai automatically handles the OAuth flow. Just use:
{
"atem": {
"url": "https://atem-mcp.cochran.cloud/"
}
}A: All existing tokens become invalid. Clients must re-authenticate via OAuth.
A: Yes! Stdio transport doesn't use OAuth - it's direct process communication. This security applies only to HTTP transport.
# 1. Generate new secret
NEW_SECRET=$(node -e "console.log(require('crypto').randomBytes(64).toString('hex'))")
# 2. Update environment
export JWT_SECRET=$NEW_SECRET
# 3. Restart server
pm2 restart atem-mcp
# 4. All clients must re-authenticateThis implementation includes usage tracking (requestCount) but not rate limiting. Consider adding:
if (stored.requestCount > 100) { // 100 requests per token
res.status(429).json({ error: 'Rate limit exceeded' });
return;
}- Issues: GitHub Issues
- Security: Email security concerns privately
- Docs: See ARCHITECTURE.md for technical details
Last Updated: April 8, 2026 Version: 1.5.0 (secure) Status: ✅ Production Ready