-
Notifications
You must be signed in to change notification settings - Fork 6
fix(mcp/oauth): Add WrapMCPEndpoint for automatic 401 handling #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ( | |
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
| "time" | ||
|
|
||
| mcpserver "github.com/mark3labs/mcp-go/server" | ||
|
|
@@ -312,6 +313,106 @@ func (s *Server) WrapHandlerFunc(next http.HandlerFunc) http.HandlerFunc { | |
| return s.WrapHandler(next).ServeHTTP | ||
| } | ||
|
|
||
| // WrapMCPEndpoint wraps an MCP endpoint handler with automatic 401 handling. | ||
| // Returns 401 with WWW-Authenticate headers if Bearer token is missing or invalid. | ||
| // | ||
| // This method provides automatic OAuth discovery for MCP clients by: | ||
| // - Passing through OPTIONS requests (CORS pre-flight) | ||
| // - Rejecting non-Bearer auth schemes (OAuth-only endpoint) | ||
| // - Returning 401 with proper headers if Bearer token is missing/malformed | ||
| // - Extracting token to context and passing to wrapped handler | ||
| // | ||
| // Usage with mark3labs SDK: | ||
| // | ||
| // streamableServer := server.NewStreamableHTTPServer(mcpServer, ...) | ||
| // mux.HandleFunc("/mcp", oauthServer.WrapMCPEndpoint(streamableServer)) | ||
| // | ||
| // For official SDK, use mcp.WithOAuth() which includes this automatically. | ||
| func (s *Server) WrapMCPEndpoint(handler http.Handler) http.HandlerFunc { | ||
| return func(w http.ResponseWriter, r *http.Request) { | ||
| // Pass through OPTIONS requests (CORS pre-flight) | ||
| if r.Method == http.MethodOptions { | ||
| handler.ServeHTTP(w, r) | ||
| return | ||
| } | ||
|
|
||
| // Check Authorization header | ||
| authHeader := r.Header.Get("Authorization") | ||
| authLower := strings.ToLower(authHeader) | ||
|
|
||
| // Return 401 if Bearer token missing | ||
| if authHeader == "" { | ||
| s.Return401(w) | ||
| return | ||
| } | ||
|
|
||
| // Check if it's a Bearer token (case-insensitive per OAuth 2.0 spec) | ||
| if !strings.HasPrefix(authLower, "bearer") { | ||
| // Reject non-Bearer schemes (OAuth endpoints require Bearer tokens only) | ||
| s.Return401(w) | ||
| return | ||
| } | ||
|
|
||
| // Malformed Bearer token (no space after "Bearer") | ||
| if !strings.HasPrefix(authLower, "bearer ") { | ||
| s.Return401InvalidToken(w) | ||
| return | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Passing through non-Bearer Authorization headers here lets a client hit the MCP endpoint with |
||
| } | ||
|
|
||
| // Extract token to context | ||
| contextFunc := CreateHTTPContextFunc() | ||
| ctx := contextFunc(r.Context(), r) | ||
| r = r.WithContext(ctx) | ||
|
|
||
| // Pass to wrapped handler | ||
| handler.ServeHTTP(w, r) | ||
| } | ||
| } | ||
|
|
||
| // Return401 writes a 401 response with WWW-Authenticate header. | ||
| // Used by WrapMCPEndpoint and can be called by adapters. | ||
| // | ||
| // Returns error code "invalid_request" per RFC 6750 §3.1 for missing tokens. | ||
| // Includes resource_metadata URL for OAuth discovery. | ||
| func (s *Server) Return401(w http.ResponseWriter) { | ||
| metadataURL := s.GetProtectedResourceMetadataURL() | ||
|
|
||
| // RFC 6750 compliant: all parameters in single Bearer header | ||
| w.Header().Set("WWW-Authenticate", fmt.Sprintf( | ||
| `Bearer realm="OAuth", error="invalid_request", error_description="Bearer token required", resource_metadata="%s"`, | ||
| metadataURL)) | ||
| w.Header().Set("Content-Type", "application/json") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| w.WriteHeader(http.StatusUnauthorized) | ||
|
|
||
| errorResponse := map[string]string{ | ||
| "error": "invalid_request", | ||
| "error_description": "Bearer token required", | ||
| } | ||
| _ = json.NewEncoder(w).Encode(errorResponse) | ||
| } | ||
|
|
||
| // Return401InvalidToken writes a 401 response for invalid/expired tokens. | ||
| // Used when token validation fails (vs missing token). | ||
| // | ||
| // Returns error code "invalid_token" per RFC 6750 §3.1 for invalid tokens. | ||
| // Includes resource_metadata URL for OAuth discovery. | ||
| func (s *Server) Return401InvalidToken(w http.ResponseWriter) { | ||
| metadataURL := s.GetProtectedResourceMetadataURL() | ||
|
|
||
| // RFC 6750 compliant: all parameters in single Bearer header | ||
| w.Header().Set("WWW-Authenticate", fmt.Sprintf( | ||
| `Bearer realm="OAuth", error="invalid_token", error_description="Authentication failed", resource_metadata="%s"`, | ||
| metadataURL)) | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(http.StatusUnauthorized) | ||
|
|
||
| errorResponse := map[string]string{ | ||
| "error": "invalid_token", | ||
| "error_description": "Authentication failed", | ||
| } | ||
| _ = json.NewEncoder(w).Encode(errorResponse) | ||
| } | ||
|
|
||
| // WithOAuth returns a server option that enables OAuth authentication | ||
| // This is the composable API for mcp-go v0.41.1 | ||
| // | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Letting non-Bearer Authorization headers fall through means anyone can send
Authorization: Basic ...and reach the MCP handler without an OAuth token, undoing the previous 401 guard and creating an auth bypass. Please keep rejecting any scheme that isn't a properly formatted Bearer token before invoking the handler.