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:

Production:

  • 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

Regular Maintenance


🚩 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.

Last updated

Was this helpful?