package middleware import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" ghcontext "github.com/github/github-mcp-server/pkg/context" "github.com/github/github-mcp-server/pkg/http/oauth" "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/utils" ) // WithScopeChallenge creates a new middleware that determines if an OAuth request contains sufficient scopes to // complete the request and returns a scope challenge if not. func WithScopeChallenge(oauthCfg *oauth.Config, scopeFetcher scopes.FetcherInterface) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Skip health check endpoints if r.URL.Path == "/_ping" { next.ServeHTTP(w, r) return } // Get user from context tokenInfo, ok := ghcontext.GetTokenInfo(ctx) if !ok { next.ServeHTTP(w, r) return } // Only check OAuth tokens - scope challenge allows OAuth apps to request additional scopes if tokenInfo.TokenType != utils.TokenTypeOAuthAccessToken { next.ServeHTTP(w, r) return } // Try to use pre-parsed MCP method info first (performance optimization) // This avoids re-parsing the JSON body if WithMCPParse middleware ran earlier var toolName string if methodInfo, ok := ghcontext.MCPMethod(ctx); ok && methodInfo != nil { // Only check tools/call requests if methodInfo.Method != "tools/call" { next.ServeHTTP(w, r) return } toolName = methodInfo.ItemName } else { // Fallback: parse the request body directly body, err := io.ReadAll(r.Body) if err != nil { next.ServeHTTP(w, r) return } r.Body = io.NopCloser(bytes.NewReader(body)) var mcpRequest struct { JSONRPC string `json:"jsonrpc"` Method string `json:"method"` Params struct { Name string `json:"name,omitempty"` Arguments map[string]any `json:"arguments,omitempty"` } `json:"params"` } err = json.Unmarshal(body, &mcpRequest) if err != nil { next.ServeHTTP(w, r) return } // Only check tools/call requests if mcpRequest.Method != "tools/call" { next.ServeHTTP(w, r) return } toolName = mcpRequest.Params.Name } toolScopeInfo, err := scopes.GetToolScopeInfo(toolName) if err != nil { next.ServeHTTP(w, r) return } // If tool not found in scope map, allow the request if toolScopeInfo == nil { next.ServeHTTP(w, r) return } // Get OAuth scopes for Token. First check if scopes are already in context, then fetch from GitHub if not present. // This allows Remote Server to pass scope info to avoid redundant GitHub API calls. activeScopes, ok := ghcontext.GetTokenScopes(ctx) if !ok || (len(activeScopes) == 0 && tokenInfo.Token != "") { activeScopes, err = scopeFetcher.FetchTokenScopes(ctx, tokenInfo.Token) if err != nil { next.ServeHTTP(w, r) return } } // Store active scopes in context for downstream use ctx = ghcontext.WithTokenScopes(ctx, activeScopes) r = r.WithContext(ctx) // Check if user has the required scopes if toolScopeInfo.HasAcceptedScope(activeScopes...) { next.ServeHTTP(w, r) return } // User lacks required scopes - get the scopes they need requiredScopes := toolScopeInfo.GetRequiredScopesSlice() // Build the resource metadata URL using the shared utility // GetEffectiveResourcePath returns the original path (e.g., /mcp or /mcp/x/all) // which is used to construct the well-known OAuth protected resource URL resourcePath := oauth.ResolveResourcePath(r, oauthCfg) resourceMetadataURL := oauth.BuildResourceMetadataURL(r, oauthCfg, resourcePath) // Build recommended scopes: existing scopes + required scopes recommendedScopes := make([]string, 0, len(activeScopes)+len(requiredScopes)) recommendedScopes = append(recommendedScopes, activeScopes...) recommendedScopes = append(recommendedScopes, requiredScopes...) // Build the WWW-Authenticate header value wwwAuthenticateHeader := fmt.Sprintf(`Bearer error="insufficient_scope", scope=%q, resource_metadata=%q, error_description=%q`, strings.Join(recommendedScopes, " "), resourceMetadataURL, "Additional scopes required: "+strings.Join(requiredScopes, ", "), ) // Send scope challenge response with the superset of existing and required scopes w.Header().Set("WWW-Authenticate", wwwAuthenticateHeader) http.Error(w, "Forbidden: insufficient scopes", http.StatusForbidden) } return http.HandlerFunc(fn) } }