OAuth 2.1 Implementation Guide
Overview
This document tracks the step-by-step implementation of OAuth 2.1 authentication for kafka-mcp-server using [email protected]. Follow this guide sequentially and update checkboxes as you complete each step.
CRITICAL ARCHITECTURAL NOTE: OAuth option MUST be passed to NewMCPServer() at creation time. This requires refactoring main.go to create the OAuth option before creating the MCP server instance.
Implementation Progress
Phase 1: Add Dependencies
Task
Add oauth-mcp-proxy library to the project.
Commands
go get github.com/tuannvm/[email protected]
go mod tidyVerification
grep "github.com/tuannvm/oauth-mcp-proxy" go.modExpected output: github.com/tuannvm/oauth-mcp-proxy v1.0.0
Phase 2: Update Configuration
File: internal/config/config.go
internal/config/config.goChanges Required
1. Add Import for strconv
Ensure strconv is imported:
import (
"os"
"strconv"
"strings"
)2. Add New Fields to Config Struct
Add after existing fields:
// HTTP Server Configuration
HTTPPort int // HTTP server port (default: 8080)
// OAuth Configuration
OAuthEnabled bool
OAuthMode string // "native" or "proxy"
OAuthProvider string // "hmac", "okta", "google", "azuread"
OAuthServerURL string // Base URL for the MCP server
// OIDC Configuration
OIDCIssuer string
OIDCClientID string
OIDCClientSecret string
OIDCAudience string
// Proxy Mode Configuration
OAuthRedirectURIs string // Comma-separated redirect URIs
JWTSecret string // Will be converted to []byte for oauth library3. Update LoadConfig Function
Add environment variable parsing (insert before the return statement):
func LoadConfig() Config {
// ... existing broker/client/transport/SASL/TLS code ...
// HTTP Port
httpPortStr := getEnv("MCP_HTTP_PORT", "8080")
httpPort, err := strconv.Atoi(httpPortStr)
if err != nil {
slog.Warn("Invalid MCP_HTTP_PORT value, using default 8080", "value", httpPortStr)
httpPort = 8080
}
// OAuth Configuration
oauthEnabledStr := getEnv("OAUTH_ENABLED", "false")
oauthEnabled, err := strconv.ParseBool(oauthEnabledStr)
if err != nil {
slog.Warn("Invalid OAUTH_ENABLED value, using default false", "value", oauthEnabledStr)
oauthEnabled = false
}
oauthMode := getEnv("OAUTH_MODE", "native")
oauthProvider := getEnv("OAUTH_PROVIDER", "okta")
oauthServerURL := getEnv("OAUTH_SERVER_URL", "")
// OIDC Configuration
oidcIssuer := getEnv("OIDC_ISSUER", "")
oidcClientID := getEnv("OIDC_CLIENT_ID", "")
oidcClientSecret := getEnv("OIDC_CLIENT_SECRET", "")
oidcAudience := getEnv("OIDC_AUDIENCE", "")
// Proxy Mode Configuration
oauthRedirectURIs := getEnv("OAUTH_REDIRECT_URIS", "")
jwtSecret := getEnv("JWT_SECRET", "")
return Config{
// ... existing fields ...
HTTPPort: httpPort,
OAuthEnabled: oauthEnabled,
OAuthMode: oauthMode,
OAuthProvider: oauthProvider,
OAuthServerURL: oauthServerURL,
OIDCIssuer: oidcIssuer,
OIDCClientID: oidcClientID,
OIDCClientSecret: oidcClientSecret,
OIDCAudience: oidcAudience,
OAuthRedirectURIs: oauthRedirectURIs,
JWTSecret: jwtSecret,
}
}Verification
Create a test file internal/config/config_test.go:
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadConfig_OAuthDefaults(t *testing.T) {
os.Clearenv()
cfg := LoadConfig()
assert.False(t, cfg.OAuthEnabled)
assert.Equal(t, "native", cfg.OAuthMode)
assert.Equal(t, "okta", cfg.OAuthProvider)
assert.Equal(t, 8080, cfg.HTTPPort)
}
func TestLoadConfig_OAuthNativeMode(t *testing.T) {
os.Clearenv()
os.Setenv("OAUTH_ENABLED", "true")
os.Setenv("OAUTH_MODE", "native")
os.Setenv("OAUTH_PROVIDER", "okta")
os.Setenv("OAUTH_SERVER_URL", "https://localhost:8080")
os.Setenv("OIDC_ISSUER", "https://company.okta.com")
os.Setenv("OIDC_AUDIENCE", "api://mcp-server")
defer os.Clearenv()
cfg := LoadConfig()
assert.True(t, cfg.OAuthEnabled)
assert.Equal(t, "native", cfg.OAuthMode)
assert.Equal(t, "okta", cfg.OAuthProvider)
assert.Equal(t, "https://localhost:8080", cfg.OAuthServerURL)
assert.Equal(t, "https://company.okta.com", cfg.OIDCIssuer)
assert.Equal(t, "api://mcp-server", cfg.OIDCAudience)
}Run test:
go test ./internal/config/... -vPhase 3: Add OAuth Helper Function
File: internal/mcp/server.go
internal/mcp/server.goChanges Required
1. Add Imports
Update imports at the top of the file:
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
oauth "github.com/tuannvm/oauth-mcp-proxy"
"github.com/tuannvm/oauth-mcp-proxy/mark3labs"
"github.com/mark3labs/mcp-go/server"
"github.com/tuannvm/kafka-mcp-server/internal/config"
)2. Add CreateOAuthOption Function
Add this function to internal/mcp/server.go:
// CreateOAuthOption creates OAuth server option if OAuth is enabled.
// This function MUST be called before creating the MCPServer instance.
//
// Returns:
// - server.ServerOption: The OAuth option to pass to NewMCPServer (nil if OAuth disabled)
// - *oauth.Server: The OAuth server instance for logging and management (nil if OAuth disabled)
// - error: Any error during OAuth setup
//
// The mux parameter must be a pre-created http.ServeMux where OAuth routes will be registered.
func CreateOAuthOption(cfg config.Config, mux *http.ServeMux) (server.ServerOption, *oauth.Server, error) {
if !cfg.OAuthEnabled {
return nil, nil, nil
}
if mux == nil {
return nil, nil, fmt.Errorf("mux is required when OAuth is enabled")
}
oauthConfig := &oauth.Config{
Provider: cfg.OAuthProvider,
Mode: cfg.OAuthMode,
Issuer: cfg.OIDCIssuer,
Audience: cfg.OIDCAudience,
ServerURL: cfg.OAuthServerURL,
}
if cfg.OAuthMode == "proxy" {
oauthConfig.ClientID = cfg.OIDCClientID
oauthConfig.ClientSecret = cfg.OIDCClientSecret
oauthConfig.RedirectURIs = cfg.OAuthRedirectURIs
oauthConfig.JWTSecret = []byte(cfg.JWTSecret)
}
oauthServer, oauthOption, err := mark3labs.WithOAuth(mux, oauthConfig)
if err != nil {
return nil, nil, fmt.Errorf("failed to setup OAuth: %w", err)
}
slog.Info("OAuth configured",
"mode", cfg.OAuthMode,
"provider", cfg.OAuthProvider,
"issuer", cfg.OIDCIssuer)
return oauthOption, oauthServer, nil
}Verification
Ensure the file compiles:
go build ./internal/mcp/...Expected: No errors
Phase 4: Refactor Main Entry Point
File: cmd/main.go
cmd/main.goChanges Required
CRITICAL: This is the most significant change. The MCP server must be created AFTER the OAuth option is prepared.
1. Add Imports
Ensure these imports are present:
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
oauth "github.com/tuannvm/oauth-mcp-proxy"
"github.com/mark3labs/mcp-go/server"
"github.com/tuannvm/kafka-mcp-server/internal/config"
"github.com/tuannvm/kafka-mcp-server/internal/kafka"
"github.com/tuannvm/kafka-mcp-server/internal/mcp"
)2. Refactor main() Function
Replace the server creation section:
func main() {
// Setup signal handling for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle SIGINT and SIGTERM
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigCh
slog.Info("Received signal, shutting down", "signal", sig)
cancel()
}()
// Load configuration
cfg := config.LoadConfig()
// Initialize Kafka client
kafkaClient, err := kafka.NewClient(cfg)
if err != nil {
slog.Error("Failed to create Kafka client", "error", err)
os.Exit(1)
}
defer kafkaClient.Close()
// Create HTTP mux and OAuth option if using HTTP transport
var mux *http.ServeMux
var oauthOption server.ServerOption
var oauthServer *oauth.Server
if cfg.MCPTransport == "http" {
mux = http.NewServeMux()
oauthOption, oauthServer, err = mcp.CreateOAuthOption(cfg, mux)
if err != nil {
slog.Error("Failed to create OAuth option", "error", err)
os.Exit(1)
}
}
// Create MCP server with OAuth option if provided
var s *server.MCPServer
if oauthOption != nil {
s = mcp.NewMCPServer("kafka-mcp-server", Version, oauthOption)
} else {
s = mcp.NewMCPServer("kafka-mcp-server", Version)
}
// Explicitly declare the client as the KafkaClient interface type
var kafkaInterface kafka.KafkaClient = kafkaClient
// Register MCP resources and tools
mcp.RegisterResources(s, kafkaInterface)
mcp.RegisterTools(s, kafkaInterface, cfg)
mcp.RegisterPrompts(s, kafkaInterface)
// Log OAuth startup info if enabled
if oauthServer != nil {
oauthServer.LogStartup(false)
}
// Start server
slog.Info("Starting Kafka MCP server", "version", Version, "transport", cfg.MCPTransport)
if err := mcp.Start(ctx, s, cfg, mux); err != nil {
slog.Error("Server error", "error", err)
os.Exit(1)
}
slog.Info("Server shutdown complete")
}Verification
Build the binary:
go build -o bin/kafka-mcp-server ./cmd/main.goExpected: Binary created without errors
Phase 5: Update Server Start Function
File: internal/mcp/server.go
internal/mcp/server.goChanges Required
Update Start Function Signature
Modify the Start function to accept the mux parameter:
// Start runs the MCP server based on the configured transport.
// For HTTP transport, mux must be provided (can be nil for stdio).
func Start(ctx context.Context, s *server.MCPServer, cfg config.Config, mux *http.ServeMux) error {
slog.Info("Starting MCP server", "transport", cfg.MCPTransport)
switch cfg.MCPTransport {
case "stdio":
return server.ServeStdio(s)
case "http":
return startHTTPServer(ctx, s, cfg, mux)
default:
return fmt.Errorf("unsupported MCP transport: %s", cfg.MCPTransport)
}
}Replace startHTTPServer Function
Replace the "HTTP transport not yet implemented" placeholder:
func startHTTPServer(ctx context.Context, s *server.MCPServer, cfg config.Config, mux *http.ServeMux) error {
if mux == nil {
return fmt.Errorf("mux is required for HTTP transport")
}
// Create StreamableHTTPServer with token extraction
// CreateHTTPContextFunc extracts Bearer tokens from Authorization header
streamable := server.NewStreamableHTTPServer(
s,
server.WithHTTPContextFunc(oauth.CreateHTTPContextFunc()),
)
mux.Handle("/mcp", streamable)
addr := fmt.Sprintf(":%d", cfg.HTTPPort)
slog.Info("Starting HTTP server",
"address", addr,
"oauth_enabled", cfg.OAuthEnabled,
"mcp_endpoint", "/mcp")
return http.ListenAndServe(addr, mux)
}Verification
Test the server in different modes:
Test STDIO Mode (Backwards Compatibility)
export MCP_TRANSPORT=stdio
go run cmd/main.goExpected: Server starts in STDIO mode, no errors
Test HTTP Mode Without OAuth
export MCP_TRANSPORT=http
export MCP_HTTP_PORT=8080
go run cmd/main.go &
sleep 1
curl http://localhost:8080/mcp
kill %1Expected: Server starts on port 8080, /mcp endpoint accessible
Phase 6: Update Documentation
File: CLAUDE.md
CLAUDE.mdAdd OAuth section after the existing configuration sections (after TLS configuration):
### OAuth Configuration (HTTP Transport Only)
OAuth 2.1 authentication is available when using HTTP transport (`MCP_TRANSPORT=http`). Supports both native and proxy modes with multiple providers (Okta, Google, Azure AD, HMAC).
**Architecture**: OAuth option must be configured before server creation. The server validates Bearer tokens in the Authorization header and makes authenticated user information available to tools via `oauth.GetUserFromContext(ctx)`.
#### Environment Variables
**HTTP Server:**
- `MCP_HTTP_PORT` - HTTP server port (default: 8080)
**OAuth Settings:**
- `OAUTH_ENABLED` - Enable OAuth (default: false)
- `OAUTH_MODE` - "native" or "proxy" (default: native)
- `OAUTH_PROVIDER` - Provider: "hmac", "okta", "google", "azuread" (default: okta)
- `OAUTH_SERVER_URL` - Full server URL (e.g., https://localhost:8080)
**OIDC Configuration:**
- `OIDC_ISSUER` - OAuth issuer URL (required when OAuth enabled)
- `OIDC_CLIENT_ID` - OAuth client ID (proxy mode only)
- `OIDC_CLIENT_SECRET` - OAuth client secret (proxy mode only)
- `OIDC_AUDIENCE` - OAuth audience (required when OAuth enabled)
**Proxy Mode Only:**
- `OAUTH_REDIRECT_URIS` - Comma-separated redirect URIs
- `JWT_SECRET` - JWT signing secret (use strong random value)
#### Native Mode Example (Okta)
Native mode: Client handles OAuth flow, server validates tokens only.
```bash
export MCP_TRANSPORT=http
export MCP_HTTP_PORT=8080
export OAUTH_ENABLED=true
export OAUTH_MODE=native
export OAUTH_PROVIDER=okta
export OAUTH_SERVER_URL=https://localhost:8080
export OIDC_ISSUER=https://company.okta.com
export OIDC_AUDIENCE=api://kafka-mcp-server
# Kafka config
export KAFKA_BROKERS=localhost:9092
make runProxy Mode Example (Google)
Proxy mode: Server manages OAuth flow and token exchange.
export MCP_TRANSPORT=http
export MCP_HTTP_PORT=8080
export OAUTH_ENABLED=true
export OAUTH_MODE=proxy
export OAUTH_PROVIDER=google
export OAUTH_SERVER_URL=https://localhost:8080
export OIDC_ISSUER=https://accounts.google.com
export OIDC_CLIENT_ID=your-client-id.apps.googleusercontent.com
export OIDC_CLIENT_SECRET=your-client-secret
export OIDC_AUDIENCE=your-client-id.apps.googleusercontent.com
export OAUTH_REDIRECT_URIS=http://localhost:8080/oauth/callback
export JWT_SECRET=$(openssl rand -hex 32)
# Kafka config
export KAFKA_BROKERS=localhost:9092
make runOAuth Endpoints
When OAuth is enabled, these endpoints are automatically registered:
/.well-known/oauth-authorization-server- OAuth 2.1 metadata (RFC 8414)/.well-known/openid-configuration- OIDC discovery/.well-known/oauth-protected-resource- Protected resource metadata/oauth/authorize- Authorization endpoint (proxy mode)/oauth/callback- Callback endpoint (proxy mode)/oauth/token- Token endpoint (proxy mode)/mcp- MCP server endpoint (protected when OAuth enabled)
Testing OAuth with HMAC Provider
For local testing without external OAuth provider:
export MCP_TRANSPORT=http
export OAUTH_ENABLED=true
export OAUTH_PROVIDER=hmac
export OAUTH_MODE=native
export OAUTH_SERVER_URL=http://localhost:8080
export OIDC_ISSUER=http://localhost:8080
export OIDC_AUDIENCE=api://kafka-mcp-server
export JWT_SECRET=$(openssl rand -hex 32)
make run
# In another terminal, check metadata
curl http://localhost:8080/.well-known/oauth-authorization-server | jqTroubleshooting
Issue: "invalid OAuth configuration" or "failed to setup OAuth"
Verify all required fields for your mode are set (see examples above)
Check OIDC_ISSUER and OIDC_AUDIENCE are valid URLs
For proxy mode, ensure OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and JWT_SECRET are set
Issue: "mux is required when OAuth is enabled"
This is an internal error; check that HTTP transport is properly configured
Issue: Token validation fails
Verify token is sent in Authorization header:
Authorization: Bearer <token>Check issuer and audience in token claims match configuration
Confirm OAuth provider is accessible from the server
Check server logs for detailed validation errors
Issue: Server starts but OAuth endpoints return 404
Verify OAuth is enabled:
OAUTH_ENABLED=trueCheck server logs for "OAuth configured" message
Ensure you're using HTTP transport, not STDIO
Security Notes
TLS Required: Always use TLS/HTTPS in production (handled at proxy/load balancer level)
Secrets Management: Never commit JWT_SECRET or OIDC_CLIENT_SECRET to version control
Token Caching: Library caches validated tokens for 5 minutes for performance
Rotation: Rotate JWT secrets regularly in proxy mode
Logging: OAuth tokens and secrets are never logged
---
## Phase 7: Unit Tests
### File: `internal/config/config_test.go`
Add comprehensive OAuth configuration tests:
```go
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadConfig_HTTPPortDefault(t *testing.T) {
os.Clearenv()
cfg := LoadConfig()
assert.Equal(t, 8080, cfg.HTTPPort)
}
func TestLoadConfig_HTTPPortCustom(t *testing.T) {
os.Clearenv()
os.Setenv("MCP_HTTP_PORT", "9090")
defer os.Clearenv()
cfg := LoadConfig()
assert.Equal(t, 9090, cfg.HTTPPort)
}
func TestLoadConfig_HTTPPortInvalid(t *testing.T) {
os.Clearenv()
os.Setenv("MCP_HTTP_PORT", "invalid")
defer os.Clearenv()
cfg := LoadConfig()
assert.Equal(t, 8080, cfg.HTTPPort) // Falls back to default
}
func TestLoadConfig_OAuthDefaults(t *testing.T) {
os.Clearenv()
cfg := LoadConfig()
assert.False(t, cfg.OAuthEnabled)
assert.Equal(t, "native", cfg.OAuthMode)
assert.Equal(t, "okta", cfg.OAuthProvider)
assert.Empty(t, cfg.OAuthServerURL)
assert.Empty(t, cfg.OIDCIssuer)
assert.Empty(t, cfg.OIDCClientID)
}
func TestLoadConfig_OAuthNativeMode(t *testing.T) {
os.Clearenv()
os.Setenv("OAUTH_ENABLED", "true")
os.Setenv("OAUTH_MODE", "native")
os.Setenv("OAUTH_PROVIDER", "okta")
os.Setenv("OAUTH_SERVER_URL", "https://localhost:8080")
os.Setenv("OIDC_ISSUER", "https://company.okta.com")
os.Setenv("OIDC_AUDIENCE", "api://mcp-server")
defer os.Clearenv()
cfg := LoadConfig()
assert.True(t, cfg.OAuthEnabled)
assert.Equal(t, "native", cfg.OAuthMode)
assert.Equal(t, "okta", cfg.OAuthProvider)
assert.Equal(t, "https://localhost:8080", cfg.OAuthServerURL)
assert.Equal(t, "https://company.okta.com", cfg.OIDCIssuer)
assert.Equal(t, "api://mcp-server", cfg.OIDCAudience)
assert.Empty(t, cfg.OIDCClientID)
assert.Empty(t, cfg.OIDCClientSecret)
}
func TestLoadConfig_OAuthProxyMode(t *testing.T) {
os.Clearenv()
os.Setenv("OAUTH_ENABLED", "true")
os.Setenv("OAUTH_MODE", "proxy")
os.Setenv("OAUTH_PROVIDER", "google")
os.Setenv("OAUTH_SERVER_URL", "https://localhost:8080")
os.Setenv("OIDC_ISSUER", "https://accounts.google.com")
os.Setenv("OIDC_CLIENT_ID", "client-id")
os.Setenv("OIDC_CLIENT_SECRET", "client-secret")
os.Setenv("OIDC_AUDIENCE", "api://mcp-server")
os.Setenv("OAUTH_REDIRECT_URIS", "http://localhost:8080/callback,http://localhost:8080/callback2")
os.Setenv("JWT_SECRET", "super-secret-key")
defer os.Clearenv()
cfg := LoadConfig()
assert.Equal(t, "proxy", cfg.OAuthMode)
assert.Equal(t, "google", cfg.OAuthProvider)
assert.Equal(t, "client-id", cfg.OIDCClientID)
assert.Equal(t, "client-secret", cfg.OIDCClientSecret)
assert.Equal(t, "http://localhost:8080/callback,http://localhost:8080/callback2", cfg.OAuthRedirectURIs)
assert.Equal(t, "super-secret-key", cfg.JWTSecret)
}
func TestLoadConfig_OAuthEnabledInvalid(t *testing.T) {
os.Clearenv()
os.Setenv("OAUTH_ENABLED", "not-a-bool")
defer os.Clearenv()
cfg := LoadConfig()
assert.False(t, cfg.OAuthEnabled) // Falls back to default
}Run tests:
go test ./internal/config/... -v -coverExpected: All tests pass
Phase 8: Integration Tests
File: internal/mcp/server_test.go
internal/mcp/server_test.goCreate HTTP server integration tests:
package mcp_test
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tuannvm/kafka-mcp-server/internal/config"
"github.com/tuannvm/kafka-mcp-server/internal/mcp"
)
func TestStartHTTPServerWithoutOAuth(t *testing.T) {
cfg := config.Config{
MCPTransport: "http",
HTTPPort: 18080,
OAuthEnabled: false,
}
mux := http.NewServeMux()
s := mcp.NewMCPServer("test", "1.0.0")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
err := mcp.Start(ctx, s, cfg, mux)
if err != nil && err != http.ErrServerClosed {
t.Logf("Server error: %v", err)
}
}()
// Wait for server to start
time.Sleep(100 * time.Millisecond)
// Test MCP endpoint exists
resp, err := http.Get("http://localhost:18080/mcp")
require.NoError(t, err)
defer resp.Body.Close()
assert.NotNil(t, resp)
cancel() // Shutdown
}
func TestCreateOAuthOption_Disabled(t *testing.T) {
cfg := config.Config{
OAuthEnabled: false,
}
mux := http.NewServeMux()
option, server, err := mcp.CreateOAuthOption(cfg, mux)
assert.NoError(t, err)
assert.Nil(t, option)
assert.Nil(t, server)
}
func TestCreateOAuthOption_NoMux(t *testing.T) {
cfg := config.Config{
OAuthEnabled: true,
}
option, server, err := mcp.CreateOAuthOption(cfg, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "mux is required")
assert.Nil(t, option)
assert.Nil(t, server)
}Run tests:
go test ./internal/mcp/... -vPhase 9: Manual Testing
Test Checklist
9.1 STDIO Mode (Backwards Compatibility)
export MCP_TRANSPORT=stdio
export KAFKA_BROKERS=localhost:9092
go run cmd/main.goExpected: Server starts successfully in STDIO mode
9.2 HTTP Mode Without OAuth
export MCP_TRANSPORT=http
export MCP_HTTP_PORT=8080
export KAFKA_BROKERS=localhost:9092
go run cmd/main.go &
# Test MCP endpoint
curl -v http://localhost:8080/mcp
# Verify no OAuth endpoints
curl -v http://localhost:8080/.well-known/oauth-authorization-server
kill %1Expected: MCP endpoint accessible, OAuth endpoints not available
9.3 HTTP Mode With Native OAuth (HMAC Provider)
export MCP_TRANSPORT=http
export MCP_HTTP_PORT=8080
export OAUTH_ENABLED=true
export OAUTH_MODE=native
export OAUTH_PROVIDER=hmac
export OAUTH_SERVER_URL=http://localhost:8080
export OIDC_ISSUER=http://localhost:8080
export OIDC_AUDIENCE=api://kafka-mcp-server
export JWT_SECRET=$(openssl rand -hex 32)
export KAFKA_BROKERS=localhost:9092
go run cmd/main.go &
# Check OAuth metadata
curl http://localhost:8080/.well-known/oauth-authorization-server | jq
# Check OIDC discovery
curl http://localhost:8080/.well-known/openid-configuration | jq
kill %1Expected:
Server logs show "OAuth configured" with provider=hmac
OAuth metadata endpoints return valid JSON
MCP endpoint at
/mcpis available
9.4 HTTP Mode With Proxy OAuth (Google - Configuration Only)
export MCP_TRANSPORT=http
export MCP_HTTP_PORT=8080
export OAUTH_ENABLED=true
export OAUTH_MODE=proxy
export OAUTH_PROVIDER=google
export OAUTH_SERVER_URL=http://localhost:8080
export OIDC_ISSUER=https://accounts.google.com
export OIDC_CLIENT_ID=test-client-id
export OIDC_CLIENT_SECRET=test-client-secret
export OIDC_AUDIENCE=test-client-id
export OAUTH_REDIRECT_URIS=http://localhost:8080/oauth/callback
export JWT_SECRET=$(openssl rand -hex 32)
export KAFKA_BROKERS=localhost:9092
go run cmd/main.go &
# Check proxy mode endpoints
curl -v http://localhost:8080/oauth/authorize
curl -v http://localhost:8080/oauth/callback
curl -v http://localhost:8080/oauth/token
kill %1Expected:
Server starts successfully
Logs show "OAuth configured" with provider=google, mode=proxy
Proxy endpoints return responses (not 404)
Phase 10: Security Review
Security Checklist
Configuration Security
Check:
# Verify secrets are not in code
grep -r "JWT_SECRET" --exclude-dir=.git --exclude="*.md" .
grep -r "CLIENT_SECRET" --exclude-dir=.git --exclude="*.md" .
# Check gitignore
cat .gitignore | grep -E "\\.env|\\.secret"Runtime Security
Deployment Security
Code Review
Progress Notes
2025-01-23 - Implementation Guide Created
Created comprehensive implementation guide
Documented critical architectural requirement: OAuth option must be passed at server creation
Provided detailed step-by-step instructions for all 10 phases
Included verification steps and test cases
2025-10-23 - OAuth Implementation Completed
Phases Completed:
✅ Phase 1: Added [email protected] dependency
✅ Phase 2: Extended Config struct with 11 OAuth fields (HTTP port, OAuth settings, OIDC config)
✅ Phase 3: Implemented CreateOAuthOption() helper function
✅ Phase 4: Refactored cmd/main.go to create OAuth option before MCPServer
✅ Phase 5: Implemented HTTP transport with StreamableHTTPServer and graceful shutdown
✅ Phase 6: Created comprehensive docs/oauth.md and updated README.md
✅ Phase 7: Wrote 21 unit tests (6 config tests + 15 MCP server tests)
Test Results:
All 21 tests passing (config + mcp packages)
Test coverage includes:
Config parsing for all OAuth fields (native/proxy modes)
CreateOAuthOption with various configurations
HTTP server startup with/without OAuth
Graceful shutdown verification
Port conflict handling
Multiple OAuth providers (HMAC, Okta, Google, Azure)
Invalid configuration handling
Edge cases (nil mux, unsupported transport, etc.)
Gemini 2.5 Pro Code Review:
No critical issues found
Architecture validated as correct
All edge cases handled properly
Graceful shutdown enhancement implemented
Key Implementation Details:
OAuth routes registered on mux before MCPServer creation
Token extraction via
oauth.CreateHTTPContextFunc()MCP endpoint exposed at
/mcpHTTP server uses context for 5-second graceful shutdown
Backwards compatible: STDIO mode unchanged
Issues Encountered
2025-10-23 - Unused Context Parameter
Issue: ctx parameter in startHTTPServer was unused
Solution: Implemented graceful shutdown using context with 5-second timeout
Impact: Better production readiness, clean server shutdown on SIGINT/SIGTERM
Time spent: 10 minutes
2025-10-23 - Missing oauth-mcp-proxy in go.mod
Issue: Initial build failed with "no required module provides package"
Solution: Ran
go get github.com/tuannvm/[email protected] && go mod tidyImpact: Dependencies properly resolved
Time spent: 2 minutes
Decisions Made
2025-10-23 - OAuth Option Architecture
Decision: Refactor main.go to create OAuth option before NewMCPServer
Rationale: Required by [email protected] API - option must be passed at server creation
Impact: Major refactor to main.go but cleaner separation of concerns
Alternative Considered: Try to add OAuth after server creation - would not work with library API
2025-10-23 - Graceful Shutdown Implementation
Decision: Use http.Server with context-based shutdown instead of http.ListenAndServe
Rationale: Gemini review identified unused ctx parameter; graceful shutdown best practice
Impact: Clean shutdown with 5-second timeout, proper resource cleanup
Code Change: Goroutine listens for ctx.Done() and calls httpServer.Shutdown()
2025-10-23 - Minimal OAuth Validation
Decision: No config validation in application code, rely on oauth-mcp-proxy library
Rationale: Keep implementation minimal, library handles validation
Impact: Cleaner code, validation errors surface at runtime with clear messages from library
Trade-off: Could add validation for better error messages, but adds complexity
2025-10-23 - Documentation Strategy
Decision: Create dedicated docs/oauth.md instead of putting everything in CLAUDE.md
Rationale: OAuth configuration is complex, deserves comprehensive standalone guide
Impact: Better user experience, easier to maintain, can reference from README
Content: Architecture diagrams, provider-specific guides, troubleshooting, security best practices
2025-10-23 - HMAC Provider JWTSecret Handling
Issue: HMAC provider requires JWTSecret in both native and proxy modes
Solution: Set JWTSecret for HMAC provider regardless of mode, then conditionally for proxy mode
Impact: HMAC provider works correctly in native mode for local testing
Code: Added separate check:
if cfg.OAuthProvider == "hmac" { oauthConfig.JWTSecret = []byte(cfg.JWTSecret) }
2025-10-23 - Provider Name Correction
Issue: Documentation used "azuread" but library expects "azure"
Solution: Updated all docs and config comments to use "azure"
Impact: Tests pass, provider name matches library expectations
Files: config.go, oauth.md, README.md
2025-10-23 - Security Review Completed
Configuration Security: ✅ No hardcoded secrets, no secret logging, all env vars documented
Runtime Security: ✅ Token validation via library, errors properly wrapped
Deployment Security: ✅ TLS documented as required, secrets rotation documented
Code Security: ✅ No credentials in code, proper error handling, input validation by library
Architecture Security: ✅ OAuth option timing correct, mux validation, graceful shutdown
Result: All security requirements met, no critical vulnerabilities found
Additional Checks: Verified HMAC provider works, provider validation, backwards compatibility maintained
Verification Commands
Quick verification after implementation:
# Check dependency installed
grep "oauth-mcp-proxy" go.mod
# Run all tests
make test-no-kafka
# Build binary
make build
# Verify binary works - STDIO mode
export MCP_TRANSPORT=stdio && ./bin/kafka-mcp-server &
sleep 1 && kill %1
# Verify binary works - HTTP mode
export MCP_TRANSPORT=http && export MCP_HTTP_PORT=8080 && ./bin/kafka-mcp-server &
sleep 1 && curl http://localhost:8080/mcp && kill %1Rollback Plan
If critical issues arise during implementation:
Quick Rollback (Development)
# Revert all uncommitted changes
git checkout .
git clean -fd
# Verify tests still pass
make test-no-kafkaSelective Rollback
Revert go.mod changes:
git checkout go.mod go.sum go mod tidyRevert config changes:
git checkout internal/config/ go test ./internal/config/...Revert server changes:
git checkout internal/mcp/server.go go build ./internal/mcp/...Revert main.go:
git checkout cmd/main.go go build -o bin/kafka-mcp-server ./cmd/main.goVerify STDIO still works:
export MCP_TRANSPORT=stdio ./bin/kafka-mcp-server
Success Criteria
Implementation is complete and successful when:
Functional Requirements
Code Quality
Documentation
Security
Testing
Implementation Status: ✅ COMPLETE
All phases completed successfully. Ready for production deployment.
Next Steps After Implementation
Create Pull Request
Ensure all tests pass
Update CHANGELOG.md
Request code review
Staging Deployment
Deploy to staging environment
Test with real OAuth provider (Okta or Google)
Verify token validation with actual tokens
Documentation
Add provider-specific setup guides (Okta, Google, Azure AD)
Create deployment runbook
Document monitoring and troubleshooting procedures
Production Preparation
Set up secrets management (e.g., Vault, AWS Secrets Manager)
Configure TLS termination at load balancer
Set up monitoring and alerting
Create rollback procedures
Support and Resources
oauth-mcp-proxy docs: https://pkg.go.dev/github.com/tuannvm/[email protected]
mcp-go docs: https://pkg.go.dev/github.com/mark3labs/[email protected]
OAuth 2.1 spec: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12
Project issues: https://github.com/tuannvm/kafka-mcp-server/issues
Last updated
Was this helpful?