go-sdk/examples/server/auth-middleware/main.go
Theodor Nesfeldt Engøy 6a81e5da2a
examples: bind auth-middleware server to localhost by default (#784)
The auth-middleware example currently defaults to `-http :8080`, which
binds to all interfaces (LAN-exposed) by default.

This changes the default to `localhost:8080` and adds a short comment
noting `-http 0.0.0.0:8080` if you intentionally want to expose it.

Rationale: this example serves token-generation endpoints and
authenticated MCP routes; binding loopback by default avoids accidental
exposure while keeping the opt-in remote behavior.

---------

Co-authored-by: Theodor N. Engøy <theodornengoy@Mac.home>
Co-authored-by: Theodor N. Engøy <theodornengoy@eduroam-193-157-246-146.wlan.uio.no>
2026-02-11 10:02:17 +01:00

396 lines
14 KiB
Go

// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"slices"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/modelcontextprotocol/go-sdk/auth"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/modelcontextprotocol/go-sdk/oauthex"
)
// This example demonstrates how to integrate auth.RequireBearerToken middleware
// with an MCP server to provide authenticated access to MCP tools and resources.
// Default to loopback to avoid exposing powerful demo endpoints to the local network.
// Set -http 0.0.0.0:8080 to intentionally expose it.
var httpAddr = flag.String("http", "localhost:8080", "HTTP address to listen on")
// JWTClaims represents the claims in our JWT tokens.
// In a real application, you would include additional claims like issuer, audience, etc.
type JWTClaims struct {
UserID string `json:"user_id"` // User identifier
Scopes []string `json:"scopes"` // Permissions/roles for the user
jwt.RegisteredClaims
}
// APIKey represents an API key with associated scopes.
// In production, this would be stored in a database with additional metadata.
type APIKey struct {
Key string `json:"key"` // The actual API key value
UserID string `json:"user_id"` // User identifier
Scopes []string `json:"scopes"` // Permissions/roles for this key
}
// In-memory storage for API keys (in production, use a database).
// This is for demonstration purposes only.
var apiKeys = map[string]*APIKey{
"sk-1234567890abcdef": {
Key: "sk-1234567890abcdef",
UserID: "user1",
Scopes: []string{"read", "write"},
},
"sk-abcdef1234567890": {
Key: "sk-abcdef1234567890",
UserID: "user2",
Scopes: []string{"read"},
},
}
// JWT secret (in production, use environment variables).
// This should be a strong, randomly generated secret in real applications.
var jwtSecret = []byte("your-secret-key")
// generateToken creates a JWT token for testing purposes.
// In a real application, this would be handled by your authentication service.
func generateToken(userID string, scopes []string, expiresIn time.Duration) (string, error) {
// Create JWT claims with user information and scopes.
claims := JWTClaims{
UserID: userID,
Scopes: scopes,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)), // Token expiration
IssuedAt: jwt.NewNumericDate(time.Now()), // Token issuance time
NotBefore: jwt.NewNumericDate(time.Now()), // Token validity start time
},
}
// Create and sign the JWT token.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// verifyJWT verifies JWT tokens and returns TokenInfo for the auth middleware.
// This function implements the TokenVerifier interface required by auth.RequireBearerToken.
func verifyJWT(ctx context.Context, tokenString string, _ *http.Request) (*auth.TokenInfo, error) {
// Parse and validate the JWT token.
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
// Verify the signing method is HMAC.
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if err != nil {
// Return standard error for invalid tokens.
return nil, fmt.Errorf("%w: %v", auth.ErrInvalidToken, err)
}
// Extract claims and verify token validity.
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return &auth.TokenInfo{
Scopes: claims.Scopes, // User permissions
Expiration: claims.ExpiresAt.Time, // Token expiration time
}, nil
}
return nil, fmt.Errorf("%w: invalid token claims", auth.ErrInvalidToken)
}
// verifyAPIKey verifies API keys and returns TokenInfo for the auth middleware.
// This function implements the TokenVerifier interface required by auth.RequireBearerToken.
func verifyAPIKey(ctx context.Context, apiKey string, _ *http.Request) (*auth.TokenInfo, error) {
// Look up the API key in our storage.
key, exists := apiKeys[apiKey]
if !exists {
return nil, fmt.Errorf("%w: API key not found", auth.ErrInvalidToken)
}
// API keys don't expire in this example, but you could add expiration logic here.
// For demonstration, we set a 24-hour expiration.
return &auth.TokenInfo{
Scopes: key.Scopes, // User permissions
Expiration: time.Now().Add(24 * time.Hour), // 24 hour expiration
}, nil
}
// MCP Tool Arguments
type getUserInfoArgs struct {
UserID string `json:"user_id" jsonschema:"the user ID to get information for"`
}
type createResourceArgs struct {
Name string `json:"name" jsonschema:"the name of the resource"`
Description string `json:"description" jsonschema:"the description of the resource"`
Content string `json:"content" jsonschema:"the content of the resource"`
}
// SayHi is a simple MCP tool that requires authentication
func SayHi(ctx context.Context, req *mcp.CallToolRequest, args struct{}) (*mcp.CallToolResult, any, error) {
// Extract user information from request (v0.3.0+)
userInfo := req.Extra.TokenInfo
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Hello! You have scopes: %v", userInfo.Scopes)},
},
}, nil, nil
}
// GetUserInfo is an MCP tool that requires read scope
func GetUserInfo(ctx context.Context, req *mcp.CallToolRequest, args getUserInfoArgs) (*mcp.CallToolResult, any, error) {
// Extract user information from request (v0.3.0+)
userInfo := req.Extra.TokenInfo
// Check if user has read scope.
if !slices.Contains(userInfo.Scopes, "read") {
return nil, nil, fmt.Errorf("insufficient permissions: read scope required")
}
userData := map[string]any{
"requested_user_id": args.UserID,
"your_scopes": userInfo.Scopes,
"message": "User information retrieved successfully",
}
userDataJSON, err := json.Marshal(userData)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal user data: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: string(userDataJSON)},
},
}, nil, nil
}
// CreateResource is an MCP tool that requires write scope
func CreateResource(ctx context.Context, req *mcp.CallToolRequest, args createResourceArgs) (*mcp.CallToolResult, any, error) {
// Extract user information from request (v0.3.0+)
userInfo := req.Extra.TokenInfo
// Check if user has write scope.
if !slices.Contains(userInfo.Scopes, "write") {
return nil, nil, fmt.Errorf("insufficient permissions: write scope required")
}
resourceInfo := map[string]any{
"name": args.Name,
"description": args.Description,
"content": args.Content,
"created_by": "authenticated_user",
"created_at": time.Now().Format(time.RFC3339),
}
resourceInfoJSON, err := json.Marshal(resourceInfo)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal resource info: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Resource created successfully: %s", string(resourceInfoJSON))},
},
}, nil, nil
}
// createMCPServer creates an MCP server with authentication-aware tools
func createMCPServer() *mcp.Server {
server := mcp.NewServer(&mcp.Implementation{Name: "authenticated-mcp-server"}, nil)
// Add tools that require authentication.
mcp.AddTool(server, &mcp.Tool{
Name: "say_hi",
Description: "A simple greeting tool that requires authentication",
}, SayHi)
mcp.AddTool(server, &mcp.Tool{
Name: "get_user_info",
Description: "Get user information (requires read scope)",
}, GetUserInfo)
mcp.AddTool(server, &mcp.Tool{
Name: "create_resource",
Description: "Create a new resource (requires write scope)",
}, CreateResource)
return server
}
func main() {
flag.Parse()
// Create the MCP server.
server := createMCPServer()
// Create authentication middleware.
jwtAuth := auth.RequireBearerToken(verifyJWT, &auth.RequireBearerTokenOptions{
Scopes: []string{"read"}, // Require "read" permission
ResourceMetadataURL: "http://localhost:8080/.well-known/oauth-protected-resource",
})
apiKeyAuth := auth.RequireBearerToken(verifyAPIKey, &auth.RequireBearerTokenOptions{
Scopes: []string{"read"}, // Require "read" permission
})
// Create HTTP handler with authentication.
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, nil)
// Apply authentication middleware to the MCP handler.
authenticatedHandler := jwtAuth(handler)
apiKeyHandler := apiKeyAuth(handler)
// Create router for different authentication methods.
http.HandleFunc("/mcp/jwt", authenticatedHandler.ServeHTTP)
http.HandleFunc("/mcp/apikey", apiKeyHandler.ServeHTTP)
// Add utility endpoints for token generation.
http.HandleFunc("/generate-token", func(w http.ResponseWriter, r *http.Request) {
// Get user ID from query parameters (default: "test-user").
userID := r.URL.Query().Get("user_id")
if userID == "" {
userID = "test-user"
}
// Get scopes from query parameters (default: ["read", "write"]).
scopes := strings.Split(r.URL.Query().Get("scopes"), ",")
if len(scopes) == 1 && scopes[0] == "" {
scopes = []string{"read", "write"}
}
// Get expiration time from query parameters (default: 1 hour).
expiresIn := 1 * time.Hour
if expStr := r.URL.Query().Get("expires_in"); expStr != "" {
if exp, err := time.ParseDuration(expStr); err == nil {
expiresIn = exp
}
}
// Generate the JWT token.
token, err := generateToken(userID, scopes, expiresIn)
if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return
}
// Return the generated token.
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
"type": "Bearer",
})
})
http.HandleFunc("/generate-api-key", func(w http.ResponseWriter, r *http.Request) {
// Generate a random API key using cryptographically secure random bytes.
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
http.Error(w, "Failed to generate random bytes", http.StatusInternalServerError)
return
}
apiKey := "sk-" + base64.URLEncoding.EncodeToString(bytes)
// Get user ID from query parameters (default: "test-user").
userID := r.URL.Query().Get("user_id")
if userID == "" {
userID = "test-user"
}
// Get scopes from query parameters (default: ["read"]).
scopes := strings.Split(r.URL.Query().Get("scopes"), ",")
if len(scopes) == 1 && scopes[0] == "" {
scopes = []string{"read"}
}
// Store the new API key in our in-memory storage.
// In production, this would be stored in a database.
apiKeys[apiKey] = &APIKey{
Key: apiKey,
UserID: userID,
Scopes: scopes,
}
// Return the generated API key.
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"api_key": apiKey,
"type": "Bearer",
})
})
// Health check endpoint.
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
"time": time.Now().Format(time.RFC3339),
})
})
// OAuth protected resource metadata endpoint.
// This endpoint provides OAuth configuration information to clients.
// CORS is enabled by default to support cross-origin client discovery.
metadata := &oauthex.ProtectedResourceMetadata{
Resource: "http://localhost:8080/mcp/jwt",
AuthorizationServers: []string{
"https://auth.example.com/.well-known/openid-configuration",
},
ScopesSupported: []string{"read", "write"},
}
http.Handle("/.well-known/oauth-protected-resource",
auth.ProtectedResourceMetadataHandler(metadata))
// Start the HTTP server.
log.Println("Authenticated MCP Server")
log.Println("========================")
log.Println("Server starting on", *httpAddr)
log.Println()
log.Println("Available endpoints:")
log.Println(" GET /health - Health check (no auth)")
log.Println(" GET /.well-known/oauth-protected-resource - OAuth resource metadata")
log.Println(" GET /generate-token - Generate JWT token")
log.Println(" POST /generate-api-key - Generate API key")
log.Println(" POST /mcp/jwt - MCP endpoint (JWT auth)")
log.Println(" POST /mcp/apikey - MCP endpoint (API key auth)")
log.Println()
log.Println("Available MCP Tools:")
log.Println(" - say_hi - Simple greeting (any auth)")
log.Println(" - get_user_info - Get user info (read scope)")
log.Println(" - create_resource - Create resource (write scope)")
log.Println()
log.Println("Example usage:")
log.Println(" # Generate a token")
log.Println(" curl 'http://localhost:8080/generate-token?user_id=alice&scopes=read,write'")
log.Println()
log.Println(" # Use MCP with JWT authentication")
log.Println(" curl -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \\")
log.Println(" -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"say_hi\",\"arguments\":{}}}' \\")
log.Println(" http://localhost:8080/mcp/jwt")
log.Println()
log.Println(" # Generate an API key")
log.Println(" curl -X POST 'http://localhost:8080/generate-api-key?user_id=bob&scopes=read'")
log.Println()
log.Println(" # Use MCP with API key authentication")
log.Println(" curl -H 'Authorization: Bearer <api_key>' -H 'Content-Type: application/json' \\")
log.Println(" -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"get_user_info\",\"arguments\":{\"user_id\":\"test\"}}}' \\")
log.Println(" http://localhost:8080/mcp/apikey")
log.Fatal(http.ListenAndServe(*httpAddr, nil))
}