Skip to content

Security: tuannvm/oauth-mcp-proxy

docs/SECURITY.md

Security Best Practices

This guide outlines security best practices when using oauth-mcp-proxy in production.


πŸ”’ Secrets Management

Never Commit Secrets

❌ BAD:

oauth.WithOAuth(mux, &oauth.Config{
    JWTSecret: []byte("my-secret-key"),  // Committed to git!
    ClientSecret: "hardcoded-secret",     // Committed to git!
})

βœ… GOOD:

oauth.WithOAuth(mux, &oauth.Config{
    JWTSecret:    []byte(os.Getenv("JWT_SECRET")),
    ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"),
})

Environment Variables

# .env (add to .gitignore!)
JWT_SECRET=your-random-32-byte-secret-key-here
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
OAUTH_ISSUER=https://yourcompany.okta.com

Load with library like godotenv:

import "github.com/joho/godotenv"

func main() {
    godotenv.Load() // Load .env file

    oauth.WithOAuth(mux, &oauth.Config{
        Provider:     os.Getenv("OAUTH_PROVIDER"),
        Issuer:       os.Getenv("OAUTH_ISSUER"),
        JWTSecret:    []byte(os.Getenv("JWT_SECRET")),
        ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"),
    })
}

.gitignore

# Secrets
.env
.env.local
.env.production

# Certificates
*.pem
*.key
*.crt

# OAuth tokens (testing)
*.token

πŸ” JWT Secret Strength (HMAC Provider)

Minimum Requirements

// Generate cryptographically secure secret
secret := make([]byte, 32)  // 32 bytes = 256 bits
if _, err := rand.Read(secret); err != nil {
    log.Fatal(err)
}

// Store as base64 or hex
secretB64 := base64.StdEncoding.EncodeToString(secret)
fmt.Println("JWT_SECRET=" + secretB64)

Validation

secret := []byte(os.Getenv("JWT_SECRET"))
if len(secret) < 32 {
    log.Fatal("JWT_SECRET must be at least 32 bytes for security")
}

Rotation

  • Rotate every: 90 days recommended
  • Process: Generate new secret β†’ Update config β†’ Deploy β†’ Update token generators
  • Zero downtime: Temporarily accept both old and new secrets during rotation

🌐 HTTPS in Production

Always Use TLS

❌ NEVER in production:

http.ListenAndServe(":80", mux)  // Unencrypted!

βœ… Production:

http.ListenAndServeTLS(":443", "server.crt", "server.key", mux)

Get Certificates

Development:

  • Use mkcert for local testing

Production:

  • Use Let's Encrypt with certbot
  • Or your cloud provider's certificate service (AWS ACM, GCP Certificate Manager)

Certificate Management

// Auto-reload certificates
certManager := &autocert.Manager{
    Prompt: autocert.AcceptTOS,
    HostPolicy: autocert.HostWhitelist("your-server.com"),
    Cache: autocert.DirCache("certs"),
}

server := &http.Server{
    Addr:      ":443",
    Handler:   mux,
    TLSConfig: certManager.TLSConfig(),
}

server.ListenAndServeTLS("", "")

🎯 Audience Validation

Why Audience Matters

Prevents token reuse across services:

Service A: Audience = "api://service-a"
Service B: Audience = "api://service-b"

Token for Service A cannot be used on Service B (even with same issuer).

Configuration

HMAC Provider:

oauth.WithOAuth(mux, &oauth.Config{
    Provider: "hmac",
    Audience: "api://my-specific-mcp-server",  // Unique per service
})

OIDC Providers:

  • Okta: Configure custom audience in auth server claims
  • Google: Use Client ID as audience
  • Azure: Use Application ID or custom App ID URI

Validation

// Token must have matching audience
{
  "aud": "api://my-specific-mcp-server",  // Must match Config.Audience
  "iss": "https://issuer.com",
  "sub": "user-123"
}

πŸ”„ Token Caching & Expiration

Cache Behavior

  • Cache TTL: 5 minutes (hardcoded in v0.1.0)
  • Cache scope: Per Server instance
  • Cache key: SHA-256 hash of token

Token Expiration Recommendations

User tokens:

  • Short-lived: 1 hour
  • Refresh tokens: 7-30 days
  • Reason: Limits damage if compromised

Service tokens:

  • Medium-lived: 6-24 hours
  • Reason: Balance between security and token refresh overhead
// When generating tokens
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "sub": "user-123",
    "aud": "api://my-server",
    "exp": time.Now().Add(1 * time.Hour).Unix(),  // Expire in 1 hour
    "iat": time.Now().Unix(),
})

πŸ›‘οΈ PKCE (Proof Key for Code Exchange)

Automatic Protection

oauth-mcp-proxy automatically supports PKCE (RFC 7636):

  • Prevents authorization code interception attacks
  • Required for public clients (mobile, desktop, browser)
  • Automatically validated when code_challenge provided

No Configuration Needed

PKCE is automatically enabled when client provides:

  • code_challenge parameter in /oauth/authorize
  • code_verifier parameter in /oauth/token

πŸšͺ Redirect URI Security

Native Mode (Client OAuth)

Localhost only for security:

βœ… http://localhost:8080/callback
βœ… http://127.0.0.1:3000/callback
βœ… http://[::1]:9000/callback
❌ http://evil.com/callback         (rejected)
❌ https://localhost.evil.com/...   (rejected - subdomain attack)

Proxy Mode (Server OAuth)

Allowlist configuration:

oauth.WithOAuth(mux, &oauth.Config{
    RedirectURIs: "https://app.example.com/callback",  // Single URI (fixed)
    // Or multiple:
    // RedirectURIs: "https://app1.com/cb,https://app2.com/cb",  // Allowlist
})

Security checks:

  • HTTPS required for non-localhost
  • No fragment allowed (per OAuth 2.0 spec)
  • Exact match validation (no wildcards)

🎫 Token Security

Token Storage (Client Side)

Browser:

  • Use httpOnly cookies or sessionStorage (NOT localStorage)
  • Clear on logout

Mobile/Desktop:

  • Use OS keychain (macOS Keychain, Windows Credential Manager)
  • Never store in plain text files

CLI Tools:

  • Store in encrypted config files
  • Use OS-specific secure storage when possible

Token Transmission

Always use Authorization header:

curl -H "Authorization: Bearer <token>" https://server.com/mcp

Never:

  • In URL query parameters (logged in web servers)
  • In cookies without httpOnly flag
  • In localStorage (XSS vulnerable)

πŸ” Logging & Monitoring

What Gets Logged

oauth-mcp-proxy logs (with custom logger or default):

Info Level:

  • Authorization requests
  • Successful authentications
  • Token cache hits

Warn Level:

  • Security violations (invalid redirects)
  • Configuration issues

Error Level:

  • Token validation failures
  • OAuth provider errors

What NOT to Log

βœ… Safe: Token hash (SHA-256)

INFO: Validating token (hash: a7bc40a987f35871...)

❌ NEVER log: Full tokens

ERROR: Token xyz123... invalid  // SECURITY VIOLATION!

Custom Logger for Production

type ProductionLogger struct {
    logger *zap.Logger
}

func (l *ProductionLogger) Error(msg string, args ...interface{}) {
    // Sanitize before logging
    l.logger.Sugar().Errorf(msg, args...)
    // Send to error tracking (Sentry, etc.)
}

oauth.WithOAuth(mux, &oauth.Config{
    Logger: &ProductionLogger{logger: zapLogger},
})

🚨 Rate Limiting

Protect OAuth Endpoints

import "golang.org/x/time/rate"

limiter := rate.NewLimiter(10, 20)  // 10 req/s, burst 20

func rateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Apply to OAuth endpoints
mux.Handle("/oauth/", rateLimitMiddleware(oauthHandler))

πŸ” Security Headers

OAuth handler automatically adds security headers:

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Cache-Control: no-store (for sensitive endpoints)

Add application-level headers:

func securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
        w.Header().Set("Content-Security-Policy", "default-src 'self'")
        next.ServeHTTP(w, r)
    })
}

http.ListenAndServeTLS(":443", "cert.pem", "key.pem", securityHeaders(mux))

πŸ“‹ Security Checklist

Pre-Production

  • All secrets in environment variables (not code)
  • HTTPS enabled with valid certificates
  • Audience configured and validated
  • JWT secret 32+ bytes (HMAC) or provider-issued (OIDC)
  • Redirect URIs properly configured
  • Token expiration set appropriately
  • Custom logger configured (no sensitive data logged)
  • Rate limiting on OAuth endpoints
  • Security headers configured

Regular Maintenance

  • Rotate secrets every 90 days
  • Review OAuth provider audit logs
  • Monitor for unusual authentication patterns
  • Update dependencies (go get -u)
  • Review token expiration policies
  • Test disaster recovery (secret compromise)

🚩 Security Incidents

Token Compromise

If JWT secret (HMAC) leaked:

  1. Generate new secret immediately
  2. Update config and redeploy
  3. All existing tokens invalidated (users must re-auth)
  4. Review logs for suspicious activity

If client secret (OIDC) leaked:

  1. Revoke in OAuth provider (Okta/Google/Azure)
  2. Generate new secret
  3. Update config and redeploy
  4. Existing user tokens still valid (not affected)

Suspicious Activity

  • Multiple failed auth attempts β†’ Consider IP blocking
  • Unusual token usage patterns β†’ Review logs
  • Invalid redirect URI attempts β†’ Security violation logged

πŸ“š Additional Resources


🀝 Reporting Security Issues

Found a security vulnerability? Email security@[your-domain] or open a confidential GitHub Security Advisory.

Do NOT open public GitHub issues for security vulnerabilities.

There aren’t any published security advisories