Common issues and solutions when using oauth-mcp-proxy.
Cause: Token not extracted from HTTP request
Solutions:
- Check Authorization header present:
# Make sure you're sending the header
curl -H "Authorization: Bearer <token>" https://server.com/mcp- Verify CreateHTTPContextFunc configured:
streamable := mcpserver.NewStreamableHTTPServer(
mcpServer,
mcpserver.WithHTTPContextFunc(oauth.CreateHTTPContextFunc()), // Required!
)- Check header format:
✅ Authorization: Bearer eyJhbGc...
❌ Authorization: eyJhbGc... (missing "Bearer ")
❌ authorization: Bearer ... (lowercase - case-sensitive!)
Cause: Token validation failed
Check:
- Token not expired:
# Decode JWT (without validation) to check expiration
echo "<token>" | cut -d. -f2 | base64 -d 2>/dev/null | jq .exp
# Compare to current Unix timestamp
date +%s- Issuer matches:
// Token's "iss" claim must match Config.Issuer exactly
Config.Issuer: "https://company.okta.com"
Token.iss: "https://company.okta.com" // Must match!- Audience matches:
// Token's "aud" claim must match Config.Audience exactly
Config.Audience: "api://my-server"
Token.aud: "api://my-server" // Must match!- Signature valid (HMAC):
// Secret must match the one used to sign token
Config.JWTSecret: []byte("secret-key-123")
// Token must be signed with same secret- Provider reachable (OIDC):
# Verify OIDC discovery works
curl https://yourcompany.okta.com/.well-known/openid-configurationDebug:
// Enable debug logging
type DebugLogger struct{}
func (l *DebugLogger) Debug(msg string, args ...interface{}) {
log.Printf("[DEBUG] "+msg, args...)
}
// ... implement Info, Warn, Error
oauth.WithOAuth(mux, &oauth.Config{
Logger: &DebugLogger{}, // See detailed validation logs
})Cause: Missing or empty Provider field
Solution:
oauth.WithOAuth(mux, &oauth.Config{
Provider: "okta", // Must be set!
// ...
})Cause: Using HMAC provider without JWTSecret
Solution:
oauth.WithOAuth(mux, &oauth.Config{
Provider: "hmac",
JWTSecret: []byte(os.Getenv("JWT_SECRET")), // Required!
})Cause: Using Okta/Google/Azure without Issuer
Solution:
oauth.WithOAuth(mux, &oauth.Config{
Provider: "okta",
Issuer: "https://yourcompany.okta.com", // Required for OIDC!
})Cause: Mode is "proxy" but ClientID not provided
Solution:
oauth.WithOAuth(mux, &oauth.Config{
Mode: "proxy",
ClientID: "your-client-id", // Required for proxy mode
ServerURL: "https://your-server.com",
RedirectURIs: "...",
})Cause: Cannot connect to OAuth provider's discovery endpoint
Check:
- Issuer URL correct:
// ✅ Correct
Issuer: "https://company.okta.com"
// ❌ Common mistakes
Issuer: "https://company.okta.com/" // Trailing slash
Issuer: "company.okta.com" // Missing https://
Issuer: "http://company.okta.com" // Must be HTTPS- Network connectivity:
# Verify server can reach provider
curl https://yourcompany.okta.com/.well-known/openid-configuration- Firewall/proxy:
- Check corporate firewall allows outbound HTTPS
- Check proxy settings if behind corporate proxy
Debug:
# Test OIDC discovery manually
curl -v https://yourcompany.okta.com/.well-known/openid-configurationCause: Client redirect is not localhost (security protection)
Fixed redirect mode only allows localhost:
✅ http://localhost:8080/callback
✅ http://127.0.0.1:3000/callback
✅ http://[::1]:9000/callback
❌ http://app.example.com/callback (not localhost)
❌ https://localhost.evil.com/... (subdomain attack)
Why: Prevents open redirect attacks in fixed redirect mode.
Solution: Use allowlist mode if you need non-localhost redirects:
RedirectURIs: "https://app1.com/cb,https://app2.com/cb" // AllowlistCause: Redirect URI not configured in OAuth provider
Solutions:
Okta:
- Go to Applications → Your App → General
- Add to "Sign-in redirect URIs"
- Must match exactly (including trailing slash if present)
Google:
- Cloud Console → Credentials → OAuth 2.0 Client
- Add to "Authorized redirect URIs"
- Exact match required
Azure:
- App registrations → Your App → Authentication
- Add to "Redirect URIs"
- Must match exactly
Expected: Second request with same token should be faster (cache hit)
Check:
- Cache logs:
[INFO] Using cached authentication for tool: hello (user: john)
-
Cache TTL: 5 minutes (hardcoded in v0.1.0)
-
Cache scope: Per Server instance
Debug:
- Different Server instances = different caches
- Token modified between requests = new cache entry
- Token expired = cache miss
Metrics:
// Check if using cached validation
// Look for "Using cached authentication" in logsCause: Usually missing logger in test code or direct handler creation
Solution:
// ✅ Always use WithOAuth() or NewServer()
oauthOption, _ := oauth.WithOAuth(mux, cfg)
// ❌ Don't create handlers directly (tests only)
handler := &OAuth2Handler{config: cfg} // Missing logger!
// ✅ In tests, include logger
handler := &OAuth2Handler{
config: cfg,
logger: &oauth.defaultLogger{}, // Or use NewOAuth2Handler()
}Cause: OAuth provider rejected token exchange request
Check:
- Authorization code valid:
- Code must be unused (single-use only)
- Code must not be expired (typically 10 minutes)
- PKCE parameters match:
// code_challenge in /authorize must match code_verifier in /token
// hash(code_verifier) == code_challenge- Redirect URI matches:
// redirect_uri in /token must match the one used in /authorize- Client credentials valid:
ClientID: "...", // Must match OAuth provider
ClientSecret: "...", // Must be current (not rotated)Debug:
- Check OAuth provider logs (Okta/Google/Azure admin consoles)
- Look for specific error codes in provider response
Expected latency:
- Cache hit: <5ms
- Cache miss (HMAC): <10ms
- Cache miss (OIDC): <100ms (network call to provider)
If slower:
- OIDC discovery slow:
- First request does OIDC discovery (fetches
.well-known/openid-configuration) - Cached after first request
- Network latency to provider affects first request
- JWKS fetch slow:
- OIDC validator fetches public keys on initialization
- Check network latency to OAuth provider
Solutions:
- Warm up on server start (make a test validation call)
- Check network connectivity to OAuth provider
- Consider caching OIDC discovery (future enhancement)
Common causes:
- HTTPS not configured:
// ❌ Development (http)
http.ListenAndServe(":8080", mux)
// ✅ Production (https)
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux)- Secrets not in environment:
# Check environment variables are set
echo $OAUTH_CLIENT_SECRET- Provider can't reach callback URL:
- ServerURL must be publicly accessible
- Firewall must allow inbound HTTPS
- DNS must resolve correctly
- Redirect URI mismatch:
- Localhost works in dev, but production URL different
- Update OAuth provider redirect URIs for production domain
type VerboseLogger struct{}
func (l *VerboseLogger) Debug(msg string, args ...interface{}) {
log.Printf("[DEBUG] "+msg, args...) // Enable debug
}
func (l *VerboseLogger) Info(msg string, args ...interface{}) {
log.Printf("[INFO] "+msg, args...)
}
func (l *VerboseLogger) Warn(msg string, args ...interface{}) {
log.Printf("[WARN] "+msg, args...)
}
func (l *VerboseLogger) Error(msg string, args ...interface{}) {
log.Printf("[ERROR] "+msg, args...)
}
oauth.WithOAuth(mux, &oauth.Config{
Logger: &VerboseLogger{},
})# Verify OAuth configuration
curl https://your-server.com/.well-known/oauth-authorization-server | jq
# Check OIDC discovery
curl https://your-server.com/.well-known/openid-configuration | jq
# Verify JWKS endpoint (OIDC providers)
curl https://your-server.com/.well-known/jwks.json | jq# Decode without verification (debugging only!)
echo "<token>" | cut -d. -f2 | base64 -d 2>/dev/null | jq
# Check claims:
# - iss matches Config.Issuer?
# - aud matches Config.Audience?
# - exp is in the future?# Generate test token (HMAC)
go run examples/mark3labs/simple/ or examples/official/simple/
# Copy token from output, test with curl
# For OIDC providers, get token from provider:
# - Okta: Use Okta test tool or API call
# - Google: Use OAuth Playground
# - Azure: Use Azure portal token tool- Check logs: Look for ERROR and WARN level messages
- Verify configuration: Review CONFIGURATION.md
- Check provider setup: Review provider-specific guide in providers/
- Security check: Review SECURITY.md
- GitHub Issues: Search or create issue at github.com/tuannvm/oauth-mcp-proxy/issues
// Create separate Server instances
oktaOption, _ := oauth.WithOAuth(mux, &oauth.Config{Provider: "okta", ...})
googleOption, _ := oauth.WithOAuth(mux, &oauth.Config{Provider: "google", ...})
// Note: Can only use one per MCP server currently
// Use environment variables to select at runtimeCurrently, oauth-mcp-proxy extracts:
sub→ User.Subjectemail→ User.Emailpreferred_username→ User.Username (fallback to email or sub)
For custom claims, access the raw token:
// Get token string from context
token, _ := oauth.GetOAuthToken(ctx)
// Parse and extract custom claims as needed- 📖 Documentation: docs/
- 💬 Discussions: GitHub Discussions (coming soon)
- 🐛 Bug Reports: GitHub Issues
- 🔒 Security: Email maintainer for confidential issues