From 06f0aeb461238aa001e9ad92132e25dcb749e6a1 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Wed, 2 Apr 2025 22:37:01 +0530 Subject: [PATCH 01/40] add --asgardeo --- cmd/proxy/main.go | 7 +++++-- config.yaml | 5 +++++ internal/config/config.go | 9 ++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 9a4b472..2308eee 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -17,6 +17,7 @@ import ( func main() { demoMode := flag.Bool("demo", false, "Use Asgardeo-based provider (demo).") + asgardeoMode := flag.Bool("asgardeo", false, "Use Asgardeo-based provider (demo).") flag.Parse() // 1. Load config @@ -32,8 +33,10 @@ func main() { cfg.JWKSURL = "https://api.asgardeo.io/t/" + cfg.Demo.OrgName + "/oauth2/jwks" provider = authz.NewAsgardeoProvider(cfg) fmt.Println("Using Asgardeo provider (demo).") - } else { - log.Fatalf("Not supported yet.") + } else if *asgardeoMode { + cfg.AuthServerBaseURL = "https://api.asgardeo.io/t/" + cfg.Asgardeo.OrgName + "/oauth2" + cfg.JWKSURL = "https://api.asgardeo.io/t/" + cfg.Asgardeo.OrgName + "/oauth2/jwks" + provider = authz.NewAsgardeoProvider(cfg) } // 3. (Optional) Fetch JWKS if you want local JWT validation diff --git a/config.yaml b/config.yaml index 9725f14..9385f58 100644 --- a/config.yaml +++ b/config.yaml @@ -16,3 +16,8 @@ demo: org_name: "openmcpauthdemo" client_id: "N0U9e_NNGr9mP_0fPnPfPI0a6twa" client_secret: "qFHfiBp5gNGAO9zV4YPnDofBzzfInatfUbHyPZvM0jka" + +asgardeo: + org_name: "" + client_id: "" + client_secret: "" diff --git a/internal/config/config.go b/internal/config/config.go index 3a3b231..6cdc949 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,6 +13,12 @@ type DemoConfig struct { OrgName string `yaml:"org_name"` } +type AsgardeoConfig struct { + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret"` + OrgName string `yaml:"org_name"` +} + type Config struct { AuthServerBaseURL string `yaml:"auth_server_base_url"` MCPServerBaseURL string `yaml:"mcp_server_base_url"` @@ -23,7 +29,8 @@ type Config struct { PathMapping map[string]string `yaml:"path_mapping"` // Nested config for Asgardeo - Demo DemoConfig `yaml:"demo"` + Demo DemoConfig `yaml:"demo"` + Asgardeo AsgardeoConfig `yaml:"asgardeo"` } // LoadConfig reads a YAML config file into Config struct. From 3d085008a86865d17f6c18afaef6de4bcfa34ade Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Wed, 2 Apr 2025 22:50:33 +0530 Subject: [PATCH 02/40] fix minor issue --- README.md | 18 ++++++++++++++++-- cmd/proxy/main.go | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c13d162..65259a2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ OpenMCPAuthProxy is a security middleware that implements the Model Context Prot ### Prerequisites - Go 1.20 or higher -- A running MCP server (SSE transport supported) ### Installation ```bash @@ -21,11 +20,26 @@ go build -o openmcpauthproxy ./cmd/proxy Create a configuration file `config.yaml` with the following parameters: +### demo mode configuration: + ```yaml mcp_server_base_url: "http://localhost:8000" # URL of your MCP server listen_address: ":8080" # Address where the proxy will listen ``` +### asgardeo configuration: + +```yaml +mcp_server_base_url: "http://localhost:8000" # URL of your MCP server +listen_address: ":8080" # Address where the proxy will listen + +asgardeo: + org_name: "your-org-name" + client_id: "your-client-id" + client_secret: "your-client-secret" + ``` + + ## Usage Example ### 1. Start the MCP Server @@ -70,7 +84,7 @@ python3 echo_server.py ./openmcpauthproxy --demo ``` -The `--demo` flag enables a demonstration mode with pre-configured authentication with [Asgardeo](https://asgardeo.io/). +The `--demo` flag enables a demonstration mode with pre-configured authentication with [Asgardeo](https://asgardeo.io/) You can also use the `--asgardeo` flag to use your own Asgardeo configuration. ### 3. Connect Using an MCP Client diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 2308eee..f02d9c3 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -17,7 +17,7 @@ import ( func main() { demoMode := flag.Bool("demo", false, "Use Asgardeo-based provider (demo).") - asgardeoMode := flag.Bool("asgardeo", false, "Use Asgardeo-based provider (demo).") + asgardeoMode := flag.Bool("asgardeo", false, "Use Asgardeo-based provider (asgardeo).") flag.Parse() // 1. Load config From 960261fc809b377f5197a88c6db501f3bb6a06e5 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Thu, 3 Apr 2025 02:53:14 +0530 Subject: [PATCH 03/40] fix standard auth --- cmd/proxy/main.go | 2 + config.yaml | 17 +++- internal/config/config.go | 9 +++ internal/proxy/proxy.go | 162 ++++++++++++++++++++++++++------------ 4 files changed, 137 insertions(+), 53 deletions(-) diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index f02d9c3..c22dc96 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -29,11 +29,13 @@ func main() { // 2. Create the chosen provider var provider authz.Provider if *demoMode { + cfg.Mode = "demo" cfg.AuthServerBaseURL = "https://api.asgardeo.io/t/" + cfg.Demo.OrgName + "/oauth2" cfg.JWKSURL = "https://api.asgardeo.io/t/" + cfg.Demo.OrgName + "/oauth2/jwks" provider = authz.NewAsgardeoProvider(cfg) fmt.Println("Using Asgardeo provider (demo).") } else if *asgardeoMode { + cfg.Mode = "asgardeo" cfg.AuthServerBaseURL = "https://api.asgardeo.io/t/" + cfg.Asgardeo.OrgName + "/oauth2" cfg.JWKSURL = "https://api.asgardeo.io/t/" + cfg.Asgardeo.OrgName + "/oauth2/jwks" provider = authz.NewAsgardeoProvider(cfg) diff --git a/config.yaml b/config.yaml index 9385f58..4c6b196 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ # config.yaml auth_server_base_url: "" -mcp_server_base_url: "http://localhost:8000" +mcp_server_base_url: "" listen_address: ":8080" jwks_url: "" timeout_seconds: 10 @@ -11,6 +11,21 @@ mcp_paths: - /sse path_mapping: + /token: /oauth/token + /.well-known/oauth-authorization-server: /.well-known/openid-configuration + +cors: + allowed_origins: + - "" + allowed_methods: + - "GET" + - "POST" + - "PUT" + - "DELETE" + allowed_headers: + - "Authorization" + - "Content-Type" + allow_credentials: true demo: org_name: "openmcpauthdemo" diff --git a/internal/config/config.go b/internal/config/config.go index 6cdc949..f4f8218 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,13 @@ type AsgardeoConfig struct { OrgName string `yaml:"org_name"` } +type CORSConfig struct { + AllowedOrigins []string `yaml:"allowed_origins"` + AllowedMethods []string `yaml:"allowed_methods"` + AllowedHeaders []string `yaml:"allowed_headers"` + AllowCredentials bool `yaml:"allow_credentials"` +} + type Config struct { AuthServerBaseURL string `yaml:"auth_server_base_url"` MCPServerBaseURL string `yaml:"mcp_server_base_url"` @@ -27,6 +34,8 @@ type Config struct { TimeoutSeconds int `yaml:"timeout_seconds"` MCPPaths []string `yaml:"mcp_paths"` PathMapping map[string]string `yaml:"path_mapping"` + Mode string `yaml:"mode"` + CORSConfig CORSConfig `yaml:"cors"` // Nested config for Asgardeo Demo DemoConfig `yaml:"demo"` diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 382c8f3..5f74125 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -20,27 +20,40 @@ import ( func NewRouter(cfg *config.Config, provider authz.Provider) http.Handler { mux := http.NewServeMux() - // 1. Custom well-known - mux.HandleFunc("/.well-known/oauth-authorization-server", provider.WellKnownHandler()) + registeredPaths := make(map[string]bool) - // 2. Registration - mux.HandleFunc("/register", provider.RegisterHandler()) + var defaultPaths []string + if cfg.Mode == "demo" || cfg.Mode == "asgardeo" { + // 1. Custom well-known + mux.HandleFunc("/.well-known/oauth-authorization-server", provider.WellKnownHandler()) + registeredPaths["/.well-known/oauth-authorization-server"] = true + + // 2. Registration + mux.HandleFunc("/register", provider.RegisterHandler()) + registeredPaths["/register"] = true + + defaultPaths = []string{"/authorize", "/token"} + } else { + defaultPaths = []string{"/authorize", "/token", "/register", "/.well-known/oauth-authorization-server"} + } - // 3. Default "auth" paths, proxied - defaultPaths := []string{"/authorize", "/token"} for _, path := range defaultPaths { mux.HandleFunc(path, buildProxyHandler(cfg)) + registeredPaths[path] = true } // 4. MCP paths for _, path := range cfg.MCPPaths { mux.HandleFunc(path, buildProxyHandler(cfg)) + registeredPaths[path] = true } - // 5. If you want to map additional paths from config.PathMapping - // to the same proxy logic: + // 5. Register paths from PathMapping that haven't been registered yet for path := range cfg.PathMapping { - mux.HandleFunc(path, buildProxyHandler(cfg)) + if !registeredPaths[path] { + mux.HandleFunc(path, buildProxyHandler(cfg)) + registeredPaths[path] = true + } } return mux @@ -57,13 +70,6 @@ func buildProxyHandler(cfg *config.Config) http.HandlerFunc { log.Fatalf("Invalid MCP server URL: %v", err) } - // We'll define sets for known auth paths, SSE paths, etc. - authPaths := map[string]bool{ - "/authorize": true, - "/token": true, - "/.well-known/oauth-authorization-server": true, - } - // Detect SSE paths from config ssePaths := make(map[string]bool) for _, p := range cfg.MCPPaths { @@ -73,23 +79,38 @@ func buildProxyHandler(cfg *config.Config) http.HandlerFunc { } return func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + allowedOrigin := getAllowedOrigin(origin, cfg) // Handle OPTIONS if r.Method == http.MethodOptions { - addCORSHeaders(w) + if allowedOrigin == "" { + log.Printf("[proxy] Preflight request from disallowed origin: %s", origin) + http.Error(w, "CORS origin not allowed", http.StatusForbidden) + return + } + addCORSHeaders(w, cfg, allowedOrigin, r.Header.Get("Access-Control-Request-Headers")) w.WriteHeader(http.StatusNoContent) return } - addCORSHeaders(w) + if allowedOrigin == "" { + log.Printf("[proxy] Request from disallowed origin: %s for %s", origin, r.URL.Path) + http.Error(w, "CORS origin not allowed", http.StatusForbidden) + return + } + + // Add CORS headers to all responses + addCORSHeaders(w, cfg, allowedOrigin, "") // Decide whether the request should go to the auth server or MCP var targetURL *url.URL isSSE := false - if authPaths[r.URL.Path] { + if isAuthPath(r.URL.Path) { targetURL = authBase } else if isMCPPath(r.URL.Path, cfg) { - // Validate JWT if you want + // Validate JWT for MCP paths if required + // Placeholder for JWT validation logic if err := util.ValidateJWT(r.Header.Get("Authorization")); err != nil { log.Printf("[proxy] Unauthorized request to %s: %v", r.URL.Path, err) http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -100,7 +121,6 @@ func buildProxyHandler(cfg *config.Config) http.HandlerFunc { isSSE = true } } else { - // If it's not recognized as an auth path or an MCP path http.Error(w, "Forbidden", http.StatusForbidden) return } @@ -120,23 +140,42 @@ func buildProxyHandler(cfg *config.Config) http.HandlerFunc { req.URL.RawQuery = r.URL.RawQuery req.Host = targetURL.Host - for header, values := range r.Header { + cleanHeaders := http.Header{} + + for k, v := range r.Header { // Skip hop-by-hop headers - if strings.EqualFold(header, "Connection") || - strings.EqualFold(header, "Keep-Alive") || - strings.EqualFold(header, "Transfer-Encoding") || - strings.EqualFold(header, "Upgrade") || - strings.EqualFold(header, "Proxy-Authorization") || - strings.EqualFold(header, "Proxy-Connection") { + if skipHeader(k) { continue } - for _, value := range values { - req.Header.Set(header, value) - } + // Set only the first value to avoid duplicates + cleanHeaders.Set(k, v[0]) } + + // Override or remove sensitive headers if needed + if strings.Contains(req.URL.Path, "/token") { + cleanHeaders.Set("Accept", "application/json") + cleanHeaders.Set("Content-Type", "application/x-www-form-urlencoded") + cleanHeaders.Set("User-Agent", "GoProxy/1.0") + cleanHeaders.Del("Origin") + cleanHeaders.Del("Referer") + } + + req.Header = cleanHeaders + + // DEBUG: log headers sent to Asgardeo + log.Println("[proxy] Outgoing request headers:") + for k, v := range req.Header { + log.Printf(" %s: %s", k, strings.Join(v, ", ")) + } + log.Printf("[proxy] %s -> %s%s", r.URL.Path, req.URL.Host, req.URL.Path) }, + ModifyResponse: func(resp *http.Response) error { + log.Printf("[proxy] Response from %s%s: %d", resp.Request.URL.Host, resp.Request.URL.Path, resp.StatusCode) + resp.Header.Del("Access-Control-Allow-Origin") // Avoid upstream conflicts + return nil + }, ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { log.Printf("[proxy] Error proxying: %v", err) http.Error(rw, "Bad Gateway", http.StatusBadGateway) @@ -156,12 +195,43 @@ func buildProxyHandler(cfg *config.Config) http.HandlerFunc { } } -func addCORSHeaders(w http.ResponseWriter) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") +func getAllowedOrigin(origin string, cfg *config.Config) string { + if origin == "" { + return cfg.CORSConfig.AllowedOrigins[0] // Default to first allowed origin + } + for _, allowed := range cfg.CORSConfig.AllowedOrigins { + if allowed == origin { + return allowed + } + } + return "" } +// addCORSHeaders adds configurable CORS headers +func addCORSHeaders(w http.ResponseWriter, cfg *config.Config, allowedOrigin, requestHeaders string) { + w.Header().Set("Access-Control-Allow-Origin", allowedOrigin) + w.Header().Set("Access-Control-Allow-Methods", strings.Join(cfg.CORSConfig.AllowedMethods, ", ")) + if requestHeaders != "" { + w.Header().Set("Access-Control-Allow-Headers", requestHeaders) + } else { + w.Header().Set("Access-Control-Allow-Headers", strings.Join(cfg.CORSConfig.AllowedHeaders, ", ")) + } + if cfg.CORSConfig.AllowCredentials { + w.Header().Set("Access-Control-Allow-Credentials", "true") + } + w.Header().Set("Vary", "Origin") +} + +func isAuthPath(path string) bool { + authPaths := map[string]bool{ + "/authorize": true, + "/token": true, + "/.well-known/oauth-authorization-server": true, + } + return authPaths[path] +} + +// isMCPPath checks if the path is an MCP path func isMCPPath(path string, cfg *config.Config) bool { for _, p := range cfg.MCPPaths { if strings.HasPrefix(path, p) { @@ -171,22 +241,10 @@ func isMCPPath(path string, cfg *config.Config) bool { return false } -func copyHeaders(src http.Header, dst http.Header) { - // Exclude hop-by-hop - hopByHop := map[string]bool{ - "Connection": true, - "Keep-Alive": true, - "Transfer-Encoding": true, - "Upgrade": true, - "Proxy-Authorization": true, - "Proxy-Connection": true, - } - for k, vv := range src { - if hopByHop[strings.ToLower(k)] { - continue - } - for _, v := range vv { - dst.Add(k, v) - } +func skipHeader(h string) bool { + switch strings.ToLower(h) { + case "connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-authorization", "proxy-connection", "te", "trailer": + return true } + return false } From a6d8eecdcc6646bc953f23b05221728239ea1a59 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Thu, 3 Apr 2025 09:32:36 +0530 Subject: [PATCH 04/40] fix ListenPort --- cmd/proxy/main.go | 7 +++++-- config.yaml | 2 +- internal/config/config.go | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index c22dc96..699929f 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -49,14 +49,17 @@ func main() { // 4. Build the main router mux := proxy.NewRouter(cfg, provider) + listen_address := fmt.Sprintf(":%d", cfg.ListenPort) + // 5. Start the server srv := &http.Server{ - Addr: cfg.ListenAddress, + + Addr: listen_address, Handler: mux, } go func() { - log.Printf("Server listening on %s", cfg.ListenAddress) + log.Printf("Server listening on %s", listen_address) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server error: %v", err) } diff --git a/config.yaml b/config.yaml index 4c6b196..ab84ad2 100644 --- a/config.yaml +++ b/config.yaml @@ -2,7 +2,7 @@ auth_server_base_url: "" mcp_server_base_url: "" -listen_address: ":8080" +listen_port: 8080 jwks_url: "" timeout_seconds: 10 diff --git a/internal/config/config.go b/internal/config/config.go index f4f8218..2f63e7c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,7 +29,7 @@ type CORSConfig struct { type Config struct { AuthServerBaseURL string `yaml:"auth_server_base_url"` MCPServerBaseURL string `yaml:"mcp_server_base_url"` - ListenAddress string `yaml:"listen_address"` + ListenPort int `yaml:"listen_port"` JWKSURL string `yaml:"jwks_url"` TimeoutSeconds int `yaml:"timeout_seconds"` MCPPaths []string `yaml:"mcp_paths"` From ec2335252cbec29f280710a23769162aaa63c9c3 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Thu, 3 Apr 2025 09:34:40 +0530 Subject: [PATCH 05/40] fix ListenPort readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65259a2..9c5b809 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,14 @@ Create a configuration file `config.yaml` with the following parameters: ```yaml mcp_server_base_url: "http://localhost:8000" # URL of your MCP server -listen_address: ":8080" # Address where the proxy will listen +listen_port: 8080 # Port where the proxy will listen ``` ### asgardeo configuration: ```yaml mcp_server_base_url: "http://localhost:8000" # URL of your MCP server -listen_address: ":8080" # Address where the proxy will listen +listen_port: 8080 # Port where the proxy will listen asgardeo: org_name: "your-org-name" From d58d93d3a1a1fa0a848437a5284e7550883b5980 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Thu, 3 Apr 2025 13:51:57 +0530 Subject: [PATCH 06/40] add default mode --- cmd/proxy/main.go | 15 ++- config.yaml | 41 ++++++- internal/authz/default.go | 94 +++++++++++++++ internal/config/config.go | 42 ++++++- internal/constants/constants.go | 7 ++ internal/proxy/modifier.go | 199 ++++++++++++++++++++++++++++++++ internal/proxy/proxy.go | 90 +++++++++++---- 7 files changed, 450 insertions(+), 38 deletions(-) create mode 100644 internal/authz/default.go create mode 100644 internal/constants/constants.go create mode 100644 internal/proxy/modifier.go diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 699929f..cde3cf3 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -11,6 +11,7 @@ import ( "github.com/wso2/open-mcp-auth-proxy/internal/authz" "github.com/wso2/open-mcp-auth-proxy/internal/config" + "github.com/wso2/open-mcp-auth-proxy/internal/constants" "github.com/wso2/open-mcp-auth-proxy/internal/proxy" "github.com/wso2/open-mcp-auth-proxy/internal/util" ) @@ -30,15 +31,19 @@ func main() { var provider authz.Provider if *demoMode { cfg.Mode = "demo" - cfg.AuthServerBaseURL = "https://api.asgardeo.io/t/" + cfg.Demo.OrgName + "/oauth2" - cfg.JWKSURL = "https://api.asgardeo.io/t/" + cfg.Demo.OrgName + "/oauth2/jwks" + cfg.AuthServerBaseURL = constants.ASGARDEO_BASE_URL + cfg.Demo.OrgName + "/oauth2" + cfg.JWKSURL = constants.ASGARDEO_BASE_URL + cfg.Demo.OrgName + "/oauth2/jwks" provider = authz.NewAsgardeoProvider(cfg) - fmt.Println("Using Asgardeo provider (demo).") } else if *asgardeoMode { cfg.Mode = "asgardeo" - cfg.AuthServerBaseURL = "https://api.asgardeo.io/t/" + cfg.Asgardeo.OrgName + "/oauth2" - cfg.JWKSURL = "https://api.asgardeo.io/t/" + cfg.Asgardeo.OrgName + "/oauth2/jwks" + cfg.AuthServerBaseURL = constants.ASGARDEO_BASE_URL + cfg.Asgardeo.OrgName + "/oauth2" + cfg.JWKSURL = constants.ASGARDEO_BASE_URL + cfg.Asgardeo.OrgName + "/oauth2/jwks" provider = authz.NewAsgardeoProvider(cfg) + } else { + cfg.Mode = "default" + cfg.JWKSURL = cfg.Default.JWKSURL + cfg.AuthServerBaseURL = cfg.Default.BaseURL + provider = authz.NewDefaultProvider(cfg) } // 3. (Optional) Fetch JWKS if you want local JWT validation diff --git a/config.yaml b/config.yaml index ab84ad2..0b0ade4 100644 --- a/config.yaml +++ b/config.yaml @@ -1,9 +1,7 @@ # config.yaml -auth_server_base_url: "" mcp_server_base_url: "" listen_port: 8080 -jwks_url: "" timeout_seconds: 10 mcp_paths: @@ -11,8 +9,10 @@ mcp_paths: - /sse path_mapping: - /token: /oauth/token - /.well-known/oauth-authorization-server: /.well-known/openid-configuration + /token: /token + /register: /register + /authorize: /authorize + /.well-known/oauth-authorization-server: /.well-known/oauth-authorization-server cors: allowed_origins: @@ -36,3 +36,36 @@ asgardeo: org_name: "" client_id: "" client_secret: "" + +default: + base_url: "" + jwks_url: "" + path: + /.well-known/oauth-authorization-server: + response: + issuer: "" + jwks_uri: "" + authorization_endpoint: "" # Optional + token_endpoint: "" # Optional + registration_endpoint: "" # Optional + response_types_supported: + - "code" + grant_types_supported: + - "authorization_code" + - "refresh_token" + code_challenge_methods_supported: + - "S256" + - "plain" + /authroize: + addQueryParams: + - name: "" + value: "" + /token: + addBodyParams: + - name: "" + value: "" + /register: + addBodyParams: + - name: "" + value: "" + diff --git a/internal/authz/default.go b/internal/authz/default.go new file mode 100644 index 0000000..9230d39 --- /dev/null +++ b/internal/authz/default.go @@ -0,0 +1,94 @@ +package authz + +import ( + "encoding/json" + "net/http" + + "github.com/wso2/open-mcp-auth-proxy/internal/config" +) + +type defaultProvider struct { + cfg *config.Config +} + +// NewDefaultProvider initializes a Provider for Asgardeo (demo mode). +func NewDefaultProvider(cfg *config.Config) Provider { + return &defaultProvider{cfg: cfg} +} + +func (p *defaultProvider) WellKnownHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Check if we have a custom response configuration + if p.cfg.Default.Path != nil { + pathConfig, exists := p.cfg.Default.Path["/.well-known/oauth-authorization-server"] + if exists && pathConfig.Response != nil { + // Use configured response values + responseConfig := pathConfig.Response + + // Get current host for proxy endpoints + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + if forwardedProto := r.Header.Get("X-Forwarded-Proto"); forwardedProto != "" { + scheme = forwardedProto + } + host := r.Host + if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" { + host = forwardedHost + } + baseURL := scheme + "://" + host + + authorizationEndpoint := responseConfig.AuthorizationEndpoint + if authorizationEndpoint == "" { + authorizationEndpoint = baseURL + "/authorize" + } + tokenEndpoint := responseConfig.TokenEndpoint + if tokenEndpoint == "" { + tokenEndpoint = baseURL + "/token" + } + registraionEndpoint := responseConfig.RegistrationEndpoint + if registraionEndpoint == "" { + registraionEndpoint = baseURL + "/register" + } + + // Build response from config + response := map[string]interface{}{ + "issuer": responseConfig.Issuer, + "authorization_endpoint": authorizationEndpoint, + "token_endpoint": tokenEndpoint, + "jwks_uri": responseConfig.JwksURI, + "response_types_supported": responseConfig.ResponseTypesSupported, + "grant_types_supported": responseConfig.GrantTypesSupported, + "token_endpoint_auth_methods_supported": []string{"client_secret_basic"}, + "registration_endpoint": registraionEndpoint, + "code_challenge_methods_supported": responseConfig.CodeChallengeMethodsSupported, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + return + } + } + } +} + +func (p *defaultProvider) RegisterHandler() http.HandlerFunc { + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index 2f63e7c..01c3a6f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,11 +26,44 @@ type CORSConfig struct { AllowCredentials bool `yaml:"allow_credentials"` } +type ParamConfig struct { + Name string `yaml:"name"` + Value string `yaml:"value"` +} + +type ResponseConfig struct { + Issuer string `yaml:"issuer,omitempty"` + JwksURI string `yaml:"jwks_uri,omitempty"` + AuthorizationEndpoint string `yaml:"authorization_endpoint,omitempty"` + TokenEndpoint string `yaml:"token_endpoint,omitempty"` + RegistrationEndpoint string `yaml:"registration_endpoint,omitempty"` + ResponseTypesSupported []string `yaml:"response_types_supported,omitempty"` + GrantTypesSupported []string `yaml:"grant_types_supported,omitempty"` + CodeChallengeMethodsSupported []string `yaml:"code_challenge_methods_supported,omitempty"` +} + +type PathConfig struct { + // For well-known endpoint + Response *ResponseConfig `yaml:"response,omitempty"` + + // For authorization endpoint + AddQueryParams []ParamConfig `yaml:"addQueryParams,omitempty"` + + // For token and register endpoints + AddBodyParams []ParamConfig `yaml:"addBodyParams,omitempty"` +} + +type DefaultConfig struct { + BaseURL string `yaml:"base_url,omitempty"` + Path map[string]PathConfig `yaml:"path,omitempty"` + JWKSURL string `yaml:"jwks_url,omitempty"` +} + type Config struct { - AuthServerBaseURL string `yaml:"auth_server_base_url"` - MCPServerBaseURL string `yaml:"mcp_server_base_url"` - ListenPort int `yaml:"listen_port"` - JWKSURL string `yaml:"jwks_url"` + AuthServerBaseURL string + MCPServerBaseURL string `yaml:"mcp_server_base_url"` + ListenPort int `yaml:"listen_port"` + JWKSURL string TimeoutSeconds int `yaml:"timeout_seconds"` MCPPaths []string `yaml:"mcp_paths"` PathMapping map[string]string `yaml:"path_mapping"` @@ -40,6 +73,7 @@ type Config struct { // Nested config for Asgardeo Demo DemoConfig `yaml:"demo"` Asgardeo AsgardeoConfig `yaml:"asgardeo"` + Default DefaultConfig `yaml:"default"` } // LoadConfig reads a YAML config file into Config struct. diff --git a/internal/constants/constants.go b/internal/constants/constants.go new file mode 100644 index 0000000..1e5808e --- /dev/null +++ b/internal/constants/constants.go @@ -0,0 +1,7 @@ +package constants + +// Package constant provides constants for the MCP Auth Proxy + +const ( + ASGARDEO_BASE_URL = "https://api.asgardeo.io/t/" +) diff --git a/internal/proxy/modifier.go b/internal/proxy/modifier.go new file mode 100644 index 0000000..8e2268b --- /dev/null +++ b/internal/proxy/modifier.go @@ -0,0 +1,199 @@ +package proxy + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/wso2/open-mcp-auth-proxy/internal/config" +) + +// RequestModifier modifies requests before they are proxied +type RequestModifier interface { + ModifyRequest(req *http.Request) (*http.Request, error) +} + +// AuthorizationModifier adds parameters to authorization requests +type AuthorizationModifier struct { + Config *config.Config +} + +// TokenModifier adds parameters to token requests +type TokenModifier struct { + Config *config.Config +} + +type RegisterModifier struct { + Config *config.Config +} + +// ModifyRequest adds configured parameters to authorization requests +func (m *AuthorizationModifier) ModifyRequest(req *http.Request) (*http.Request, error) { + // Check if we have parameters to add + if m.Config.Default.Path == nil { + return req, nil + } + + pathConfig, exists := m.Config.Default.Path["/authorize"] + if !exists || len(pathConfig.AddQueryParams) == 0 { + return req, nil + } + // Get current query parameters + query := req.URL.Query() + + // Add parameters from config + for _, param := range pathConfig.AddQueryParams { + query.Set(param.Name, param.Value) + } + + // Update the request URL + req.URL.RawQuery = query.Encode() + + return req, nil +} + +// ModifyRequest adds configured parameters to token requests +func (m *TokenModifier) ModifyRequest(req *http.Request) (*http.Request, error) { + // Only modify POST requests + if req.Method != http.MethodPost { + return req, nil + } + + // Check if we have parameters to add + if m.Config.Default.Path == nil { + return req, nil + } + + pathConfig, exists := m.Config.Default.Path["/token"] + if !exists || len(pathConfig.AddBodyParams) == 0 { + return req, nil + } + + contentType := req.Header.Get("Content-Type") + + if strings.Contains(contentType, "application/x-www-form-urlencoded") { + // Parse form data + if err := req.ParseForm(); err != nil { + return nil, err + } + + // Clone form data + formData := req.PostForm + + // Add configured parameters + for _, param := range pathConfig.AddBodyParams { + formData.Set(param.Name, param.Value) + } + + // Create new request body with modified form + formEncoded := formData.Encode() + req.Body = io.NopCloser(strings.NewReader(formEncoded)) + req.ContentLength = int64(len(formEncoded)) + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(formEncoded))) + + } else if strings.Contains(contentType, "application/json") { + // Read body + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + + // Parse JSON + var jsonData map[string]interface{} + if err := json.Unmarshal(bodyBytes, &jsonData); err != nil { + return nil, err + } + + // Add parameters + for _, param := range pathConfig.AddBodyParams { + jsonData[param.Name] = param.Value + } + + // Marshal back to JSON + modifiedBody, err := json.Marshal(jsonData) + if err != nil { + return nil, err + } + + // Update request + req.Body = io.NopCloser(bytes.NewReader(modifiedBody)) + req.ContentLength = int64(len(modifiedBody)) + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(modifiedBody))) + } + + return req, nil +} + +func (m *RegisterModifier) ModifyRequest(req *http.Request) (*http.Request, error) { + // Only modify POST requests + if req.Method != http.MethodPost { + return req, nil + } + + // Check if we have parameters to add + if m.Config.Default.Path == nil { + return req, nil + } + + pathConfig, exists := m.Config.Default.Path["/register"] + if !exists || len(pathConfig.AddBodyParams) == 0 { + return req, nil + } + + contentType := req.Header.Get("Content-Type") + + if strings.Contains(contentType, "application/x-www-form-urlencoded") { + // Parse form data + if err := req.ParseForm(); err != nil { + return nil, err + } + + // Clone form data + formData := req.PostForm + + // Add configured parameters + for _, param := range pathConfig.AddBodyParams { + formData.Set(param.Name, param.Value) + } + + // Create new request body with modified form + formEncoded := formData.Encode() + req.Body = io.NopCloser(strings.NewReader(formEncoded)) + req.ContentLength = int64(len(formEncoded)) + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(formEncoded))) + + } else if strings.Contains(contentType, "application/json") { + // Read body + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + + // Parse JSON + var jsonData map[string]interface{} + if err := json.Unmarshal(bodyBytes, &jsonData); err != nil { + return nil, err + } + + // Add parameters + for _, param := range pathConfig.AddBodyParams { + jsonData[param.Name] = param.Value + } + + // Marshal back to JSON + modifiedBody, err := json.Marshal(jsonData) + if err != nil { + return nil, err + } + + // Update request + req.Body = io.NopCloser(bytes.NewReader(modifiedBody)) + req.ContentLength = int64(len(modifiedBody)) + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(modifiedBody))) + } + + return req, nil +} diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 5f74125..c999be4 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -20,38 +20,77 @@ import ( func NewRouter(cfg *config.Config, provider authz.Provider) http.Handler { mux := http.NewServeMux() + modifiers := map[string]RequestModifier{ + "/authorize": &AuthorizationModifier{Config: cfg}, + "/token": &TokenModifier{Config: cfg}, + "/register": &RegisterModifier{Config: cfg}, + } + registeredPaths := make(map[string]bool) var defaultPaths []string + + // Handle based on mode configuration if cfg.Mode == "demo" || cfg.Mode == "asgardeo" { - // 1. Custom well-known + // Demo/Asgardeo mode: Custom handlers for well-known and register mux.HandleFunc("/.well-known/oauth-authorization-server", provider.WellKnownHandler()) registeredPaths["/.well-known/oauth-authorization-server"] = true - // 2. Registration mux.HandleFunc("/register", provider.RegisterHandler()) registeredPaths["/register"] = true + // Authorize and token will be proxied with parameter modification defaultPaths = []string{"/authorize", "/token"} } else { - defaultPaths = []string{"/authorize", "/token", "/register", "/.well-known/oauth-authorization-server"} + // Default provider mode + if cfg.Default.Path != nil { + // Check if we have custom response for well-known + wellKnownConfig, exists := cfg.Default.Path["/.well-known/oauth-authorization-server"] + if exists && wellKnownConfig.Response != nil { + // If there's a custom response defined, use our handler + mux.HandleFunc("/.well-known/oauth-authorization-server", provider.WellKnownHandler()) + registeredPaths["/.well-known/oauth-authorization-server"] = true + } else { + // No custom response, add well-known to proxy paths + defaultPaths = append(defaultPaths, "/.well-known/oauth-authorization-server") + } + + defaultPaths = append(defaultPaths, "/authorize") + defaultPaths = append(defaultPaths, "/token") + defaultPaths = append(defaultPaths, "/register") + } else { + defaultPaths = []string{"/authorize", "/token", "/register", "/.well-known/oauth-authorization-server"} + } } + // Remove duplicates from defaultPaths + uniquePaths := make(map[string]bool) + cleanPaths := []string{} + for _, path := range defaultPaths { + if !uniquePaths[path] { + uniquePaths[path] = true + cleanPaths = append(cleanPaths, path) + } + } + defaultPaths = cleanPaths + for _, path := range defaultPaths { - mux.HandleFunc(path, buildProxyHandler(cfg)) - registeredPaths[path] = true + if !registeredPaths[path] { + mux.HandleFunc(path, buildProxyHandler(cfg, modifiers)) + registeredPaths[path] = true + } } - // 4. MCP paths + // MCP paths for _, path := range cfg.MCPPaths { - mux.HandleFunc(path, buildProxyHandler(cfg)) + mux.HandleFunc(path, buildProxyHandler(cfg, modifiers)) registeredPaths[path] = true } - // 5. Register paths from PathMapping that haven't been registered yet + // Register paths from PathMapping that haven't been registered yet for path := range cfg.PathMapping { if !registeredPaths[path] { - mux.HandleFunc(path, buildProxyHandler(cfg)) + mux.HandleFunc(path, buildProxyHandler(cfg, modifiers)) registeredPaths[path] = true } } @@ -59,8 +98,9 @@ func NewRouter(cfg *config.Config, provider authz.Provider) http.Handler { return mux } -func buildProxyHandler(cfg *config.Config) http.HandlerFunc { +func buildProxyHandler(cfg *config.Config, modifiers map[string]RequestModifier) http.HandlerFunc { // Parse the base URLs up front + authBase, err := url.Parse(cfg.AuthServerBaseURL) if err != nil { log.Fatalf("Invalid auth server URL: %v", err) @@ -125,6 +165,17 @@ func buildProxyHandler(cfg *config.Config) http.HandlerFunc { return } + // Apply request modifiers to add parameters + if modifier, exists := modifiers[r.URL.Path]; exists { + var err error + r, err = modifier.ModifyRequest(r) + if err != nil { + log.Printf("[proxy] Error modifying request: %v", err) + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + } + // Build the reverse proxy rp := &httputil.ReverseProxy{ Director: func(req *http.Request) { @@ -152,23 +203,8 @@ func buildProxyHandler(cfg *config.Config) http.HandlerFunc { cleanHeaders.Set(k, v[0]) } - // Override or remove sensitive headers if needed - if strings.Contains(req.URL.Path, "/token") { - cleanHeaders.Set("Accept", "application/json") - cleanHeaders.Set("Content-Type", "application/x-www-form-urlencoded") - cleanHeaders.Set("User-Agent", "GoProxy/1.0") - cleanHeaders.Del("Origin") - cleanHeaders.Del("Referer") - } - req.Header = cleanHeaders - // DEBUG: log headers sent to Asgardeo - log.Println("[proxy] Outgoing request headers:") - for k, v := range req.Header { - log.Printf(" %s: %s", k, strings.Join(v, ", ")) - } - log.Printf("[proxy] %s -> %s%s", r.URL.Path, req.URL.Host, req.URL.Path) }, ModifyResponse: func(resp *http.Response) error { @@ -226,8 +262,12 @@ func isAuthPath(path string) bool { authPaths := map[string]bool{ "/authorize": true, "/token": true, + "/register": true, "/.well-known/oauth-authorization-server": true, } + if strings.HasPrefix(path, "/u/") { + return true + } return authPaths[path] } From 0a2019a6a9dd82738da467beb32fdfe58a0413c2 Mon Sep 17 00:00:00 2001 From: Ayesha Dissanayaka Date: Thu, 3 Apr 2025 13:57:26 +0530 Subject: [PATCH 07/40] Link integration with existing OAuth providers (#6) --- README.md | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b2fb23c..b587878 100644 --- a/README.md +++ b/README.md @@ -89,32 +89,7 @@ asgardeo: ./openmcpauthproxy --asgardeo ``` -### Use with Auth0 - -Enable authorization for the MCP server through your Auth0 organization - -**TODO**: Add instructions - -[Enable dynamic application registration](https://auth0.com/docs/get-started/applications/dynamic-client-registration#enable-dynamic-client-registration) in your Auth0 organization - -#### Configure the Auth Proxy - -Create a configuration file config.yaml with the following parameters: - -```yaml -mcp_server_base_url: "http://localhost:8000" # URL of your MCP server -listen_address: ":8080" # Address where the proxy will listen -``` - -**TODO**: Update the configs for Auth0. - -#### Start the Auth Proxy - -```bash -./openmcpauthproxy --auth0 -``` - -### Use with a standard OAuth Server +### Use with any standard OAuth Server Enable authorization for the MCP server with a compliant OAuth server @@ -133,3 +108,6 @@ listen_address: ":8080" # Address where the proxy will lis ```bash ./openmcpauthproxy ``` +#### Integrating with existing OAuth Providers + + - [Auth0](URL) - Enable authorization for the MCP server through your Auth0 organization. **TODO**: Add instructions under docs and link From cc15b8c2769eaaae7294d7b5973804206ed96c4a Mon Sep 17 00:00:00 2001 From: Ayesha Dissanayaka Date: Thu, 3 Apr 2025 14:27:40 +0530 Subject: [PATCH 08/40] Improve diagrams --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 258f848..38623c0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The Open MCP Auth Proxy is a lightweight proxy designed to sit in front of MCP servers and enforce authorization in compliance with the [Model Context Protocol authorization](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/) requirements. It intercepts incoming requests, validates tokens, and offloads authentication and authorization to an OAuth-compliant Identity Provider. -![image](https://github.com/user-attachments/assets/fc728670-2fdb-4a63-bcc4-b9b6a6c8b4ba) +![image](https://github.com/user-attachments/assets/41cf6723-c488-4860-8640-8fec45006f92) ## **Setup and Installation** From 8dddce0f2c8f35bb4e5bb59706dac467ea103e3d Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Thu, 3 Apr 2025 15:20:34 +0530 Subject: [PATCH 09/40] add auth0 docs --- docs/Auth0.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/Auth0.md diff --git a/docs/Auth0.md b/docs/Auth0.md new file mode 100644 index 0000000..d5762b5 --- /dev/null +++ b/docs/Auth0.md @@ -0,0 +1,85 @@ +## Integrating with Auth0 + +This guide will help you configure Open MCP Auth Proxy to use Auth0 as your identity provider. + +### Prerequisites + +- An Auth0 organization (sign up here if you don't have one) +- Open MCP Auth Proxy installed + +### Setting Up Auth0 +1. [Enable Dynamic Client Registration](https://auth0.com/docs/get-started/applications/dynamic-client-registration) + - Go to your Auth0 dashboard + - Navigate to Settings > Advanced + - Enable "OIDC Dynamic Application Registration" +2. Inorder to setup connections in dynamically created clients [promote Connections to Domain Level](https://auth0.com/docs/authenticate/identity-providers/promote-connections-to-domain-level) +3. Create an API in Auth0: + - Go to your Auth0 dashboard + - Navigate to Applications > APIs + - Click on "Create API" + - Set a Name (e.g., "MCP API") + - Set an Identifier (e.g., "mcp_proxy") + - Keep the default signing algorithm (RS256) + - Click "Create" + +### Configuring the Open MCP Auth Proxy + +Update your `config.yaml` with Auth0 settings: + +```yaml +# Basic proxy configuration +mcp_server_base_url: "http://localhost:8000" +listen_port: 8080 +timeout_seconds: 10 + +# CORS configuration +cors: + allowed_origins: + - "http://localhost:5173" # Your client application origin + allowed_methods: + - "GET" + - "POST" + - "PUT" + - "DELETE" + allowed_headers: + - "Authorization" + - "Content-Type" + allow_credentials: true + +# Path mappings for Auth0 endpoints +path_mapping: + /token: /oauth/token + /register: /oidc/register + +# Auth0 configuration +default: + base_url: "https://YOUR_AUTH0_DOMAIN" # e.g., https://dev-123456.us.auth0.com + jwks_url: "https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json" + path: + /.well-known/oauth-authorization-server: + response: + issuer: "https://YOUR_AUTH0_DOMAIN/" + jwks_uri: "https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json" + authorization_endpoint: "https://YOUR_AUTH0_DOMAIN/authorize?audience=mcp_proxy" # Only if you created an API with this identifier + response_types_supported: + - "code" + grant_types_supported: + - "authorization_code" + - "refresh_token" + code_challenge_methods_supported: + - "S256" + - "plain" + /token: + addBodyParams: + - name: "audience" + value: "mcp_proxy" # Only if you created an API with this identifier +``` + +Replace YOUR_AUTH0_DOMAIN with your Auth0 domain (e.g., dev-abc123.us.auth0.com). + +## Starting the Proxy with Auth0 Integration +Start the proxy in default mode (which will use Auth0 based on your configuration): + +```bash +./openmcpauthproxy +``` From 8ac6207c7d86ee03051db73bd409e32176d3d030 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Thu, 3 Apr 2025 15:25:02 +0530 Subject: [PATCH 10/40] link auth0 docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 258f848..ab5348e 100644 --- a/README.md +++ b/README.md @@ -112,4 +112,4 @@ listen_address: ":8080" # Address where the proxy will lis ``` #### Integrating with existing OAuth Providers - - [Auth0](URL) - Enable authorization for the MCP server through your Auth0 organization. **TODO**: Add instructions under docs and link + - [Auth0](docs/Auth0.md) - Enable authorization for the MCP server through your Auth0 organization. From 31bcaee30ab4051dac8fd4de2ef34425961dddfc Mon Sep 17 00:00:00 2001 From: Omindu Rathnaweera Date: Thu, 3 Apr 2025 16:00:42 +0530 Subject: [PATCH 11/40] Update docs/Auth0.md Co-authored-by: Pavindu Lakshan --- docs/Auth0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Auth0.md b/docs/Auth0.md index d5762b5..fe55edc 100644 --- a/docs/Auth0.md +++ b/docs/Auth0.md @@ -12,7 +12,7 @@ This guide will help you configure Open MCP Auth Proxy to use Auth0 as your iden - Go to your Auth0 dashboard - Navigate to Settings > Advanced - Enable "OIDC Dynamic Application Registration" -2. Inorder to setup connections in dynamically created clients [promote Connections to Domain Level](https://auth0.com/docs/authenticate/identity-providers/promote-connections-to-domain-level) +2. In order to setup connections in dynamically created clients [promote Connections to Domain Level](https://auth0.com/docs/authenticate/identity-providers/promote-connections-to-domain-level) 3. Create an API in Auth0: - Go to your Auth0 dashboard - Navigate to Applications > APIs From d31ee539011797cd853b58f94f56d3ea314fce3f Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Thu, 3 Apr 2025 16:56:32 +0530 Subject: [PATCH 12/40] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 44f1144..eaeab21 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Update the following parameters in `config.yaml`. ```yaml mcp_server_base_url: "http://localhost:8000" # URL of your MCP server -listen_address: ":8080" # Address where the proxy will listen +listen_port: 8080 # Address where the proxy will listen ``` #### Start the Auth Proxy @@ -58,7 +58,7 @@ The `--demo` flag enables a demonstration mode with pre-configured authenticatio #### Connect Using an MCP Client -You can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test the connection and try out the complete authorization flow. +You can use the [MCP Inspector](https://github.com/shashimalcse/inspector) to test the connection and try out the complete authorization flow. ### Use with Asgardeo @@ -101,7 +101,7 @@ Create a configuration file config.yaml with the following parameters: ```yaml mcp_server_base_url: "http://localhost:8000" # URL of your MCP server -listen_address: ":8080" # Address where the proxy will listen +listen_port: 8080 # Address where the proxy will listen ``` **TODO**: Update the configs for a standard OAuth Server. From 07df7cd0a639677688d0f2708ebbc360fb05e0e9 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Thu, 3 Apr 2025 16:57:45 +0530 Subject: [PATCH 13/40] Update port config --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eaeab21..d153faa 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Create a configuration file config.yaml with the following parameters: ```yaml mcp_server_base_url: "http://localhost:8000" # URL of your MCP server -listen_address: ":8080" # Address where the proxy will listen +listen_port: 8080 # Address where the proxy will listen asgardeo: org_name: "" # Your Asgardeo org name From 97ceeb3a1d976b712e3f8ea88a6020bab1e84ff9 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath <43197743+shashimalcse@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:37:28 +0530 Subject: [PATCH 14/40] Update readme with reason for the inspector fork (#10) * add reason for fork --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d153faa..0ba80d9 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The `--demo` flag enables a demonstration mode with pre-configured authenticatio #### Connect Using an MCP Client -You can use the [MCP Inspector](https://github.com/shashimalcse/inspector) to test the connection and try out the complete authorization flow. +You can use this fork of the [MCP Inspector](https://github.com/shashimalcse/inspector) to test the connection and try out the complete authorization flow. (this is a temporary fork with fixes for authentication issues in the original implementation) ### Use with Asgardeo From 48c7f30ea82b2c3d6e6278b6032cf1dd2fbc134d Mon Sep 17 00:00:00 2001 From: Ayesha Dissanayaka Date: Thu, 3 Apr 2025 20:00:02 +0530 Subject: [PATCH 15/40] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ba80d9..0837ea2 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The `--demo` flag enables a demonstration mode with pre-configured authenticatio #### Connect Using an MCP Client -You can use this fork of the [MCP Inspector](https://github.com/shashimalcse/inspector) to test the connection and try out the complete authorization flow. (this is a temporary fork with fixes for authentication issues in the original implementation) +You can use this fork of the [MCP Inspector](https://github.com/shashimalcse/inspector) to test the connection and try out the complete authorization flow. (This is a temporary fork with fixes for authentication [issues](https://github.com/modelcontextprotocol/typescript-sdk/issues/257) in the original implementation) ### Use with Asgardeo From 28f830dfbf24a4dba0db1ff9240f1410a76eb338 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath <43197743+shashimalcse@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:02:12 +0530 Subject: [PATCH 16/40] remove default config (#11) --- config.yaml | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/config.yaml b/config.yaml index 0b0ade4..b949380 100644 --- a/config.yaml +++ b/config.yaml @@ -9,14 +9,10 @@ mcp_paths: - /sse path_mapping: - /token: /token - /register: /register - /authorize: /authorize - /.well-known/oauth-authorization-server: /.well-known/oauth-authorization-server cors: allowed_origins: - - "" + - "http://localhost:5173" allowed_methods: - "GET" - "POST" @@ -32,40 +28,5 @@ demo: client_id: "N0U9e_NNGr9mP_0fPnPfPI0a6twa" client_secret: "qFHfiBp5gNGAO9zV4YPnDofBzzfInatfUbHyPZvM0jka" -asgardeo: - org_name: "" - client_id: "" - client_secret: "" -default: - base_url: "" - jwks_url: "" - path: - /.well-known/oauth-authorization-server: - response: - issuer: "" - jwks_uri: "" - authorization_endpoint: "" # Optional - token_endpoint: "" # Optional - registration_endpoint: "" # Optional - response_types_supported: - - "code" - grant_types_supported: - - "authorization_code" - - "refresh_token" - code_challenge_methods_supported: - - "S256" - - "plain" - /authroize: - addQueryParams: - - name: "" - value: "" - /token: - addBodyParams: - - name: "" - value: "" - /register: - addBodyParams: - - name: "" - value: "" From 8d7aab073e9ba59a60b0baf7614e40ed69dc43c0 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 4 Apr 2025 14:08:11 +0530 Subject: [PATCH 17/40] Add instructions to run the sample MCP server --- .gitignore | 1 + README.md | 18 ++++++++++++++++-- resources/requirements.txt | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 resources/requirements.txt diff --git a/.gitignore b/.gitignore index 6c1dd97..2a2b503 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ *.zip *.tar.gz *.rar +.venv # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* diff --git a/README.md b/README.md index 0837ea2..85ab0d5 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,22 @@ go build -o openmcpauthproxy ./cmd/proxy Allows you to just enable authentication and authorization for your MCP server with the preconfigured auth provider powered by Asgardeo. If you don’t have an MCP server, follow the instructions given here to start your own MCP server for testing purposes. -1. Download [sample MCP server](resources/echo_server.py) -2. Run the server with + +1. Navigate to `resources` directory. +2. Initialize a virtual environment. + +```bash +python3 -m venv .venv +``` + +3. Install dependencies. + +``` +pip3 install -r requirements.txt +``` + +4. Start the server. + ```bash python3 echo_server.py ``` diff --git a/resources/requirements.txt b/resources/requirements.txt new file mode 100644 index 0000000..102b728 --- /dev/null +++ b/resources/requirements.txt @@ -0,0 +1 @@ +fastmcp==0.4.1 \ No newline at end of file From 6ce52261db90ae77fab1082e9233eba46bdead8e Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 4 Apr 2025 14:45:58 +0530 Subject: [PATCH 18/40] Add venv activate step --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85ab0d5..d00f16e 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,19 @@ If you don’t have an MCP server, follow the instructions given here to start y ```bash python3 -m venv .venv ``` +3. Activate virtual environment. -3. Install dependencies. +```bash +source .venv/bin/activate +``` + +4. Install dependencies. ``` pip3 install -r requirements.txt ``` -4. Start the server. +5. Start the server. ```bash python3 echo_server.py From 32c9378aad94039385a6c2927cb9da859b8fbc95 Mon Sep 17 00:00:00 2001 From: Chiran Fernando Date: Tue, 8 Apr 2025 12:46:00 +0530 Subject: [PATCH 19/40] Add transport mode support for stdio, SSE stability fixes (#13) Add transport mode support for stdio, SSE stability fixes --- README.md | 255 +++++++++++++++++++++---------- cmd/proxy/main.go | 78 ++++++++-- config.yaml | 28 +++- internal/authz/asgardeo.go | 23 ++- internal/authz/default.go | 2 + internal/config/config.go | 114 ++++++++++++-- internal/logging/logger.go | 34 +++++ internal/proxy/modifier.go | 5 + internal/proxy/proxy.go | 60 +++++--- internal/proxy/sse.go | 79 +++++++++- internal/subprocess/manager.go | 268 +++++++++++++++++++++++++++++++++ internal/util/jwks.go | 4 +- 12 files changed, 808 insertions(+), 142 deletions(-) create mode 100644 internal/logging/logger.go create mode 100644 internal/subprocess/manager.go diff --git a/README.md b/README.md index d00f16e..f197891 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,81 @@ # Open MCP Auth Proxy -The Open MCP Auth Proxy is a lightweight proxy designed to sit in front of MCP servers and enforce authorization in compliance with the [Model Context Protocol authorization](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/) requirements. It intercepts incoming requests, validates tokens, and offloads authentication and authorization to an OAuth-compliant Identity Provider. +A lightweight authorization proxy for Model Context Protocol (MCP) servers that enforces authorization according to the [MCP authorization specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/). -![image](https://github.com/user-attachments/assets/41cf6723-c488-4860-8640-8fec45006f92) +![Architecture Diagram](https://github.com/user-attachments/assets/41cf6723-c488-4860-8640-8fec45006f92) -## **Setup and Installation** +## What it Does -### **Prerequisites** +Open MCP Auth Proxy sits between MCP clients and your MCP server to: -* Go 1.20 or higher -* A running MCP server (SSE transport supported) -* An MCP client that supports MCP authorization +- Intercept incoming requests +- Validate authorization tokens +- Offload authentication and authorization to OAuth-compliant Identity Providers +- Support the MCP authorization protocol -### **Installation** +## Quick Start + +### Prerequisites + +* Go 1.20 or higher +* A running MCP server +* An MCP client that supports MCP authorization + +### Installation ```bash -git clone https://github.com/wso2/open-mcp-auth-proxy -cd open-mcp-auth-proxy - -go get github.com/golang-jwt/jwt/v4 -go get gopkg.in/yaml.v2 - +git clone https://github.com/wso2/open-mcp-auth-proxy +cd open-mcp-auth-proxy +go get github.com/golang-jwt/jwt/v4 gopkg.in/yaml.v2 go build -o openmcpauthproxy ./cmd/proxy ``` -## Using Open MCP Auth Proxy +### Basic Usage -### Quick Start - -Allows you to just enable authentication and authorization for your MCP server with the preconfigured auth provider powered by Asgardeo. - -If you don’t have an MCP server, follow the instructions given here to start your own MCP server for testing purposes. - -1. Navigate to `resources` directory. -2. Initialize a virtual environment. - -```bash -python3 -m venv .venv -``` -3. Activate virtual environment. - -```bash -source .venv/bin/activate -``` - -4. Install dependencies. - -``` -pip3 install -r requirements.txt -``` - -5. Start the server. - -```bash -python3 echo_server.py -``` - -#### Configure the Auth Proxy - -Update the following parameters in `config.yaml`. - -### demo mode configuration: +1. The repository comes with a default `config.yaml` file that contains the basic configuration: ```yaml -mcp_server_base_url: "http://localhost:8000" # URL of your MCP server -listen_port: 8080 # Address where the proxy will listen +listen_port: 8080 +base_url: "http://localhost:8000" # Your MCP server URL +paths: + sse: "/sse" + messages: "/messages/" ``` -#### Start the Auth Proxy +2. Start the proxy in demo mode (uses pre-configured authentication with Asgardeo sandbox): ```bash ./openmcpauthproxy --demo ``` -The `--demo` flag enables a demonstration mode with pre-configured authentication and authorization with a sandbox powered by [Asgardeo](https://asgardeo.io/). +3. Connect using an MCP client like [MCP Inspector](https://github.com/shashimalcse/inspector)(This is a temporary fork with fixes for authentication [issues](https://github.com/modelcontextprotocol/typescript-sdk/issues/257) in the original implementation) -#### Connect Using an MCP Client +## Identity Provider Integration -You can use this fork of the [MCP Inspector](https://github.com/shashimalcse/inspector) to test the connection and try out the complete authorization flow. (This is a temporary fork with fixes for authentication [issues](https://github.com/modelcontextprotocol/typescript-sdk/issues/257) in the original implementation) +### Demo Mode -### Use with Asgardeo +For quick testing, use the `--demo` flag which includes pre-configured authentication and authorization with an Asgardeo sandbox. -Enable authorization for the MCP server through your own Asgardeo organization +```bash +./openmcpauthproxy --demo +``` -1. [Register]([url](https://asgardeo.io/signup)) and create an organization in Asgardeo -2. Now, you need to authorize the OpenMCPAuthProxy to allow dynamically registering MCP Clients as applications in your organization. To do that, - 1. Create an [M2M application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/) - 1. [Authorize this application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/#authorize-the-api-resources-for-the-app) to invoke “Application Management API” with the `internal_application_mgt_create` scope. - ![image](https://github.com/user-attachments/assets/0bd57cac-1904-48cc-b7aa-0530224bc41a) - 2. Note the **Client ID** and **Client secret** of this application. This is required by the auth proxy +### Asgardeo Integration + +To enable authorization through your own Asgardeo organization: + +1. [Register](https://asgardeo.io/signup) and create an organization in Asgardeo +2. Create an [M2M application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/) + 1. [Authorize this application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/#authorize-the-api-resources-for-the-app) to invoke "Application Management API" with the `internal_application_mgt_create` scope + ![image](https://github.com/user-attachments/assets/0bd57cac-1904-48cc-b7aa-0530224bc41a) + 2. Update the existing `config.yaml` with your Asgardeo details: #### Configure the Auth Proxy Create a configuration file config.yaml with the following parameters: ```yaml -mcp_server_base_url: "http://localhost:8000" # URL of your MCP server +base_url: "http://localhost:8000" # URL of your MCP server listen_port: 8080 # Address where the proxy will listen asgardeo: @@ -104,31 +84,146 @@ asgardeo: client_secret: "" # Client secret of the M2M app ``` -#### Start the Auth Proxy +3. Start the proxy with Asgardeo integration: ```bash ./openmcpauthproxy --asgardeo ``` -### Use with any standard OAuth Server +### Other OAuth Providers -Enable authorization for the MCP server with a compliant OAuth server +- [Auth0 Integration Guide](docs/Auth0.md) -#### Configuration +## Testing with an Example MCP Server -Create a configuration file config.yaml with the following parameters: +If you don't have an MCP server, you can use the included example: -```yaml -mcp_server_base_url: "http://localhost:8000" # URL of your MCP server -listen_port: 8080 # Address where the proxy will listen -``` -**TODO**: Update the configs for a standard OAuth Server. - -#### Start the Auth Proxy +1. Navigate to the `resources` directory +2. Set up a Python environment: ```bash -./openmcpauthproxy +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt ``` -#### Integrating with existing OAuth Providers - - [Auth0](docs/Auth0.md) - Enable authorization for the MCP server through your Auth0 organization. +3. Start the example server: + +```bash +python3 echo_server.py +``` + +# Advanced Configuration + +### Transport Modes + +The proxy supports two transport modes: + +- **SSE Mode (Default)**: For Server-Sent Events transport +- **stdio Mode**: For MCP servers that use stdio transport + +When using stdio mode, the proxy: +- Starts an MCP server as a subprocess using the command specified in the configuration +- Communicates with the subprocess through standard input/output (stdio) +- **Note**: Any commands specified (like `npx` in the example below) must be installed on your system first + +To use stdio mode: + +```bash +./openmcpauthproxy --demo --stdio +``` + +#### Example: Running an MCP Server as a Subprocess + +1. Configure stdio mode in your `config.yaml`: + +```yaml +listen_port: 8080 +base_url: "http://localhost:8000" + +stdio: + enabled: true + user_command: "npx -y @modelcontextprotocol/server-github" # Example using a GitHub MCP server + env: # Environment variables (optional) + - "GITHUB_PERSONAL_ACCESS_TOKEN=gitPAT" + +# CORS configuration +cors: + allowed_origins: + - "http://localhost:5173" # Origin of your client application + allowed_methods: + - "GET" + - "POST" + - "PUT" + - "DELETE" + allowed_headers: + - "Authorization" + - "Content-Type" + allow_credentials: true + +# Demo configuration for Asgardeo +demo: + org_name: "openmcpauthdemo" + client_id: "N0U9e_NNGr9mP_0fPnPfPI0a6twa" + client_secret: "qFHfiBp5gNGAO9zV4YPnDofBzzfInatfUbHyPZvM0jka" +``` + +2. Run the proxy with stdio mode: + +```bash +./openmcpauthproxy --demo +``` + +The proxy will: +- Start the MCP server as a subprocess using the specified command +- Handle all authorization requirements +- Forward messages between clients and the server + +### Complete Configuration Reference + +```yaml +# Common configuration +listen_port: 8080 +base_url: "http://localhost:8000" +port: 8000 + +# Path configuration +paths: + sse: "/sse" + messages: "/messages/" + +# Transport mode +transport_mode: "sse" # Options: "sse" or "stdio" + +# stdio-specific configuration (used only in stdio mode) +stdio: + enabled: true + user_command: "npx -y @modelcontextprotocol/server-github" # Command to start the MCP server (requires npx to be installed) + work_dir: "" # Optional working directory for the subprocess + +# CORS configuration +cors: + allowed_origins: + - "http://localhost:5173" + allowed_methods: + - "GET" + - "POST" + - "PUT" + - "DELETE" + allowed_headers: + - "Authorization" + - "Content-Type" + allow_credentials: true + +# Demo configuration for Asgardeo +demo: + org_name: "openmcpauthdemo" + client_id: "N0U9e_NNGr9mP_0fPnPfPI0a6twa" + client_secret: "qFHfiBp5gNGAO9zV4YPnDofBzzfInatfUbHyPZvM0jka" + +# Asgardeo configuration (used with --asgardeo flag) +asgardeo: + org_name: "" + client_id: "" + client_secret: "" +``` \ No newline at end of file diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index cde3cf3..6424f18 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -3,31 +3,71 @@ package main import ( "flag" "fmt" - "log" "net/http" "os" "os/signal" + "syscall" "time" "github.com/wso2/open-mcp-auth-proxy/internal/authz" "github.com/wso2/open-mcp-auth-proxy/internal/config" "github.com/wso2/open-mcp-auth-proxy/internal/constants" + "github.com/wso2/open-mcp-auth-proxy/internal/logging" "github.com/wso2/open-mcp-auth-proxy/internal/proxy" + "github.com/wso2/open-mcp-auth-proxy/internal/subprocess" "github.com/wso2/open-mcp-auth-proxy/internal/util" ) func main() { demoMode := flag.Bool("demo", false, "Use Asgardeo-based provider (demo).") asgardeoMode := flag.Bool("asgardeo", false, "Use Asgardeo-based provider (asgardeo).") + debugMode := flag.Bool("debug", false, "Enable debug logging") + stdioMode := flag.Bool("stdio", false, "Use stdio transport mode instead of SSE") flag.Parse() + logger.SetDebug(*debugMode) + // 1. Load config cfg, err := config.LoadConfig("config.yaml") if err != nil { - log.Fatalf("Error loading config: %v", err) + logger.Error("Error loading config: %v", err) + os.Exit(1) } - // 2. Create the chosen provider + // Override transport mode if stdio flag is set + if *stdioMode { + cfg.TransportMode = config.StdioTransport + // Ensure stdio is enabled + cfg.Stdio.Enabled = true + // Re-validate config + if err := cfg.Validate(); err != nil { + logger.Error("Configuration error: %v", err) + os.Exit(1) + } + } + + logger.Info("Using transport mode: %s", cfg.TransportMode) + logger.Info("Using MCP server base URL: %s", cfg.BaseURL) + logger.Info("Using MCP paths: SSE=%s, Messages=%s", cfg.Paths.SSE, cfg.Paths.Messages) + + // 2. Start subprocess if configured and in stdio mode + var procManager *subprocess.Manager + if cfg.TransportMode == config.StdioTransport && cfg.Stdio.Enabled { + // Ensure all required dependencies are available + if err := subprocess.EnsureDependenciesAvailable(cfg.Stdio.UserCommand); err != nil { + logger.Warn("%v", err) + logger.Warn("Subprocess may fail to start due to missing dependencies") + } + + procManager = subprocess.NewManager() + if err := procManager.Start(cfg); err != nil { + logger.Warn("Failed to start subprocess: %v", err) + } + } else if cfg.TransportMode == config.SSETransport { + logger.Info("Using SSE transport mode, not starting subprocess") + } + + // 3. Create the chosen provider var provider authz.Provider if *demoMode { cfg.Mode = "demo" @@ -46,41 +86,49 @@ func main() { provider = authz.NewDefaultProvider(cfg) } - // 3. (Optional) Fetch JWKS if you want local JWT validation + // 4. (Optional) Fetch JWKS if you want local JWT validation if err := util.FetchJWKS(cfg.JWKSURL); err != nil { - log.Fatalf("Failed to fetch JWKS: %v", err) + logger.Error("Failed to fetch JWKS: %v", err) + os.Exit(1) } - // 4. Build the main router + // 5. Build the main router mux := proxy.NewRouter(cfg, provider) listen_address := fmt.Sprintf(":%d", cfg.ListenPort) - // 5. Start the server + // 6. Start the server srv := &http.Server{ - Addr: listen_address, Handler: mux, } go func() { - log.Printf("Server listening on %s", listen_address) + logger.Info("Server listening on %s", listen_address) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Server error: %v", err) + logger.Error("Server error: %v", err) + os.Exit(1) } }() - // 6. Graceful shutdown on Ctrl+C + // 7. Wait for shutdown signal stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop - log.Println("Shutting down...") + logger.Info("Shutting down...") + // 8. First terminate subprocess if running + if procManager != nil && procManager.IsRunning() { + procManager.Shutdown() + } + + // 9. Then shutdown the server + logger.Info("Shutting down HTTP server...") shutdownCtx, cancel := proxy.NewShutdownContext(5 * time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { - log.Printf("Shutdown error: %v", err) + logger.Error("HTTP server shutdown error: %v", err) } - log.Println("Stopped.") + logger.Info("Stopped.") } diff --git a/config.yaml b/config.yaml index b949380..971b93c 100644 --- a/config.yaml +++ b/config.yaml @@ -1,15 +1,31 @@ # config.yaml -mcp_server_base_url: "" +# Common configuration for all transport modes listen_port: 8080 +base_url: "http://localhost:8000" # Base URL for the MCP server +port: 8000 # Port for the MCP server timeout_seconds: 10 -mcp_paths: - - /messages/ - - /sse +# Path configuration +paths: + sse: "/sse" # SSE endpoint path + messages: "/messages/" # Messages endpoint path +# Transport mode configuration +transport_mode: "sse" # Options: "sse" or "stdio" + +# stdio-specific configuration (used only when transport_mode is "stdio") +stdio: + enabled: true + user_command: "npx -y @modelcontextprotocol/server-github" + work_dir: "" # Working directory (optional) + # env: # Environment variables (optional) + # - "NODE_ENV=development" + +# Path mapping (optional) path_mapping: +# CORS configuration cors: allowed_origins: - "http://localhost:5173" @@ -23,10 +39,8 @@ cors: - "Content-Type" allow_credentials: true +# Demo configuration for Asgardeo demo: org_name: "openmcpauthdemo" client_id: "N0U9e_NNGr9mP_0fPnPfPI0a6twa" client_secret: "qFHfiBp5gNGAO9zV4YPnDofBzzfInatfUbHyPZvM0jka" - - - diff --git a/internal/authz/asgardeo.go b/internal/authz/asgardeo.go index 7408f79..a3c812c 100644 --- a/internal/authz/asgardeo.go +++ b/internal/authz/asgardeo.go @@ -7,13 +7,13 @@ import ( "encoding/json" "fmt" "io" - "log" "math/rand" "net/http" "strings" "time" "github.com/wso2/open-mcp-auth-proxy/internal/config" + "github.com/wso2/open-mcp-auth-proxy/internal/logging" ) type asgardeoProvider struct { @@ -31,6 +31,7 @@ func (p *asgardeoProvider) WellKnownHandler() http.HandlerFunc { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("X-Accel-Buffering", "no") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) @@ -70,8 +71,9 @@ func (p *asgardeoProvider) WellKnownHandler() http.HandlerFunc { } w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-Accel-Buffering", "no") if err := json.NewEncoder(w).Encode(response); err != nil { - log.Printf("[asgardeoProvider] Error encoding well-known: %v", err) + logger.Error("Error encoding well-known: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } } @@ -83,6 +85,7 @@ func (p *asgardeoProvider) RegisterHandler() http.HandlerFunc { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("X-Accel-Buffering", "no") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) @@ -95,7 +98,7 @@ func (p *asgardeoProvider) RegisterHandler() http.HandlerFunc { var regReq RegisterRequest if err := json.NewDecoder(r.Body).Decode(®Req); err != nil { - log.Printf("ERROR: reading register request: %v", err) + logger.Error("Reading register request: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } @@ -109,7 +112,7 @@ func (p *asgardeoProvider) RegisterHandler() http.HandlerFunc { regReq.ClientSecret = randomString(16) if err := p.createAsgardeoApplication(regReq); err != nil { - log.Printf("WARN: Asgardeo application creation failed: %v", err) + logger.Warn("Asgardeo application creation failed: %v", err) // Optionally http.Error(...) if you want to fail // or continue to return partial data. } @@ -124,9 +127,10 @@ func (p *asgardeoProvider) RegisterHandler() http.HandlerFunc { } w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-Accel-Buffering", "no") w.WriteHeader(http.StatusCreated) if err := json.NewEncoder(w).Encode(resp); err != nil { - log.Printf("ERROR: encoding /register response: %v", err) + logger.Error("Encoding /register response: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } } @@ -186,7 +190,7 @@ func (p *asgardeoProvider) createAsgardeoApplication(regReq RegisterRequest) err return fmt.Errorf("Asgardeo creation error (%d): %s", resp.StatusCode, string(respBody)) } - log.Printf("INFO: Created Asgardeo application for clientID=%s", regReq.ClientID) + logger.Info("Created Asgardeo application for clientID=%s", regReq.ClientID) return nil } @@ -202,8 +206,11 @@ func (p *asgardeoProvider) getAsgardeoAdminToken() (string, error) { } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + // Sensitive data - should not be logged at INFO level auth := p.cfg.Demo.ClientID + ":" + p.cfg.Demo.ClientSecret req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) + + logger.Debug("Requesting admin token for Asgardeo with client ID: %s", p.cfg.Demo.ClientID) tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, @@ -234,6 +241,10 @@ func (p *asgardeoProvider) getAsgardeoAdminToken() (string, error) { return "", fmt.Errorf("failed to parse token JSON: %w", err) } + // Don't log the actual token at info level, only at debug level + logger.Debug("Received access token: %s", tokenResp.AccessToken) + logger.Info("Successfully obtained admin token from Asgardeo") + return tokenResp.AccessToken, nil } diff --git a/internal/authz/default.go b/internal/authz/default.go index 9230d39..929f586 100644 --- a/internal/authz/default.go +++ b/internal/authz/default.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/wso2/open-mcp-auth-proxy/internal/config" + "github.com/wso2/open-mcp-auth-proxy/internal/logging" ) type defaultProvider struct { @@ -81,6 +82,7 @@ func (p *defaultProvider) WellKnownHandler() http.HandlerFunc { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { + logger.Error("Error encoding well-known response: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } return diff --git a/internal/config/config.go b/internal/config/config.go index 01c3a6f..fc6743c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,12 +1,35 @@ package config import ( + "fmt" "os" "gopkg.in/yaml.v2" ) -// AsgardeoConfig groups all Asgardeo-specific fields +// Transport mode for MCP server +type TransportMode string + +const ( + SSETransport TransportMode = "sse" + StdioTransport TransportMode = "stdio" +) + +// Common path configuration for all transport modes +type PathsConfig struct { + SSE string `yaml:"sse"` + Messages string `yaml:"messages"` +} + +// StdioConfig contains stdio-specific configuration +type StdioConfig struct { + Enabled bool `yaml:"enabled"` + UserCommand string `yaml:"user_command"` // The command provided by the user + WorkDir string `yaml:"work_dir"` // Working directory (optional) + Args []string `yaml:"args,omitempty"` // Additional arguments + Env []string `yaml:"env,omitempty"` // Environment variables +} + type DemoConfig struct { ClientID string `yaml:"client_id"` ClientSecret string `yaml:"client_secret"` @@ -60,15 +83,18 @@ type DefaultConfig struct { } type Config struct { - AuthServerBaseURL string - MCPServerBaseURL string `yaml:"mcp_server_base_url"` - ListenPort int `yaml:"listen_port"` - JWKSURL string - TimeoutSeconds int `yaml:"timeout_seconds"` - MCPPaths []string `yaml:"mcp_paths"` - PathMapping map[string]string `yaml:"path_mapping"` - Mode string `yaml:"mode"` - CORSConfig CORSConfig `yaml:"cors"` + AuthServerBaseURL string + ListenPort int `yaml:"listen_port"` + BaseURL string `yaml:"base_url"` + Port int `yaml:"port"` + JWKSURL string + TimeoutSeconds int `yaml:"timeout_seconds"` + PathMapping map[string]string `yaml:"path_mapping"` + Mode string `yaml:"mode"` + CORSConfig CORSConfig `yaml:"cors"` + TransportMode TransportMode `yaml:"transport_mode"` + Paths PathsConfig `yaml:"paths"` + Stdio StdioConfig `yaml:"stdio"` // Nested config for Asgardeo Demo DemoConfig `yaml:"demo"` @@ -76,6 +102,56 @@ type Config struct { Default DefaultConfig `yaml:"default"` } +// Validate checks if the config is valid based on transport mode +func (c *Config) Validate() error { + // Validate based on transport mode + if c.TransportMode == StdioTransport { + if !c.Stdio.Enabled { + return fmt.Errorf("stdio.enabled must be true in stdio transport mode") + } + if c.Stdio.UserCommand == "" { + return fmt.Errorf("stdio.user_command is required in stdio transport mode") + } + } + + // Validate paths + if c.Paths.SSE == "" { + c.Paths.SSE = "/sse" // Default value + } + if c.Paths.Messages == "" { + c.Paths.Messages = "/messages" // Default value + } + + // Validate base URL + if c.BaseURL == "" { + if c.Port > 0 { + c.BaseURL = fmt.Sprintf("http://localhost:%d", c.Port) + } else { + c.BaseURL = "http://localhost:8000" // Default value + } + } + + return nil +} + +// GetMCPPaths returns the list of paths that should be proxied to the MCP server +func (c *Config) GetMCPPaths() []string { + return []string{c.Paths.SSE, c.Paths.Messages} +} + +// BuildExecCommand constructs the full command string for execution in stdio mode +func (c *Config) BuildExecCommand() string { + if c.Stdio.UserCommand == "" { + return "" + } + + // Construct the full command + return fmt.Sprintf( + `npx -y supergateway --stdio "%s" --port %d --baseUrl %s --ssePath %s --messagePath %s`, + c.Stdio.UserCommand, c.Port, c.BaseURL, c.Paths.SSE, c.Paths.Messages, + ) +} + // LoadConfig reads a YAML config file into Config struct. func LoadConfig(path string) (*Config, error) { f, err := os.Open(path) @@ -89,8 +165,26 @@ func LoadConfig(path string) (*Config, error) { if err := decoder.Decode(&cfg); err != nil { return nil, err } + + // Set default values if cfg.TimeoutSeconds == 0 { cfg.TimeoutSeconds = 15 // default } + + // Set default transport mode if not specified + if cfg.TransportMode == "" { + cfg.TransportMode = SSETransport // Default to SSE + } + + // Set default port if not specified + if cfg.Port == 0 { + cfg.Port = 8000 // default + } + + // Validate the configuration + if err := cfg.Validate(); err != nil { + return nil, err + } + return &cfg, nil } diff --git a/internal/logging/logger.go b/internal/logging/logger.go new file mode 100644 index 0000000..57bec27 --- /dev/null +++ b/internal/logging/logger.go @@ -0,0 +1,34 @@ +package logger + +import ( + "log" +) + +var isDebug = false + +// SetDebug enables or disables debug logging +func SetDebug(debug bool) { + isDebug = debug +} + +// Debug logs a debug-level message +func Debug(format string, v ...interface{}) { + if isDebug { + log.Printf("DEBUG: "+format, v...) + } +} + +// Info logs an info-level message +func Info(format string, v ...interface{}) { + log.Printf("INFO: "+format, v...) +} + +// Warn logs a warning-level message +func Warn(format string, v ...interface{}) { + log.Printf("WARN: "+format, v...) +} + +// Error logs an error-level message +func Error(format string, v ...interface{}) { + log.Printf("ERROR: "+format, v...) +} diff --git a/internal/proxy/modifier.go b/internal/proxy/modifier.go index 8e2268b..6662b2c 100644 --- a/internal/proxy/modifier.go +++ b/internal/proxy/modifier.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/wso2/open-mcp-auth-proxy/internal/config" + "github.com/wso2/open-mcp-auth-proxy/internal/logging" ) // RequestModifier modifies requests before they are proxied @@ -148,6 +149,7 @@ func (m *RegisterModifier) ModifyRequest(req *http.Request) (*http.Request, erro if strings.Contains(contentType, "application/x-www-form-urlencoded") { // Parse form data if err := req.ParseForm(); err != nil { + logger.Error("Failed to parse form data: %v", err) return nil, err } @@ -169,12 +171,14 @@ func (m *RegisterModifier) ModifyRequest(req *http.Request) (*http.Request, erro // Read body bodyBytes, err := io.ReadAll(req.Body) if err != nil { + logger.Error("Failed to read request body: %v", err) return nil, err } // Parse JSON var jsonData map[string]interface{} if err := json.Unmarshal(bodyBytes, &jsonData); err != nil { + logger.Error("Failed to parse JSON body: %v", err) return nil, err } @@ -186,6 +190,7 @@ func (m *RegisterModifier) ModifyRequest(req *http.Request) (*http.Request, erro // Marshal back to JSON modifiedBody, err := json.Marshal(jsonData) if err != nil { + logger.Error("Failed to marshal modified JSON: %v", err) return nil, err } diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index c999be4..33a9ea3 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -2,7 +2,6 @@ package proxy import ( "context" - "log" "net/http" "net/http/httputil" "net/url" @@ -11,6 +10,7 @@ import ( "github.com/wso2/open-mcp-auth-proxy/internal/authz" "github.com/wso2/open-mcp-auth-proxy/internal/config" + "github.com/wso2/open-mcp-auth-proxy/internal/logging" "github.com/wso2/open-mcp-auth-proxy/internal/util" ) @@ -82,7 +82,8 @@ func NewRouter(cfg *config.Config, provider authz.Provider) http.Handler { } // MCP paths - for _, path := range cfg.MCPPaths { + mcpPaths := cfg.GetMCPPaths() + for _, path := range mcpPaths { mux.HandleFunc(path, buildProxyHandler(cfg, modifiers)) registeredPaths[path] = true } @@ -100,23 +101,21 @@ func NewRouter(cfg *config.Config, provider authz.Provider) http.Handler { func buildProxyHandler(cfg *config.Config, modifiers map[string]RequestModifier) http.HandlerFunc { // Parse the base URLs up front - authBase, err := url.Parse(cfg.AuthServerBaseURL) if err != nil { - log.Fatalf("Invalid auth server URL: %v", err) + logger.Error("Invalid auth server URL: %v", err) + panic(err) // Fatal error that prevents startup } - mcpBase, err := url.Parse(cfg.MCPServerBaseURL) + + mcpBase, err := url.Parse(cfg.BaseURL) if err != nil { - log.Fatalf("Invalid MCP server URL: %v", err) + logger.Error("Invalid MCP server URL: %v", err) + panic(err) // Fatal error that prevents startup } // Detect SSE paths from config ssePaths := make(map[string]bool) - for _, p := range cfg.MCPPaths { - if p == "/sse" { - ssePaths[p] = true - } - } + ssePaths[cfg.Paths.SSE] = true return func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") @@ -124,7 +123,7 @@ func buildProxyHandler(cfg *config.Config, modifiers map[string]RequestModifier) // Handle OPTIONS if r.Method == http.MethodOptions { if allowedOrigin == "" { - log.Printf("[proxy] Preflight request from disallowed origin: %s", origin) + logger.Warn("Preflight request from disallowed origin: %s", origin) http.Error(w, "CORS origin not allowed", http.StatusForbidden) return } @@ -134,7 +133,7 @@ func buildProxyHandler(cfg *config.Config, modifiers map[string]RequestModifier) } if allowedOrigin == "" { - log.Printf("[proxy] Request from disallowed origin: %s for %s", origin, r.URL.Path) + logger.Warn("Request from disallowed origin: %s for %s", origin, r.URL.Path) http.Error(w, "CORS origin not allowed", http.StatusForbidden) return } @@ -152,7 +151,7 @@ func buildProxyHandler(cfg *config.Config, modifiers map[string]RequestModifier) // Validate JWT for MCP paths if required // Placeholder for JWT validation logic if err := util.ValidateJWT(r.Header.Get("Authorization")); err != nil { - log.Printf("[proxy] Unauthorized request to %s: %v", r.URL.Path, err) + logger.Warn("Unauthorized request to %s: %v", r.URL.Path, err) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } @@ -170,7 +169,7 @@ func buildProxyHandler(cfg *config.Config, modifiers map[string]RequestModifier) var err error r, err = modifier.ModifyRequest(r) if err != nil { - log.Printf("[proxy] Error modifying request: %v", err) + logger.Error("Error modifying request: %v", err) http.Error(w, "Bad Request", http.StatusBadRequest) return } @@ -192,7 +191,13 @@ func buildProxyHandler(cfg *config.Config, modifiers map[string]RequestModifier) req.Host = targetURL.Host cleanHeaders := http.Header{} - + + // Set proper origin header to match the target + if isSSE { + // For SSE, ensure origin matches the target + req.Header.Set("Origin", targetURL.Scheme+"://"+targetURL.Host) + } + for k, v := range r.Header { // Skip hop-by-hop headers if skipHeader(k) { @@ -205,21 +210,33 @@ func buildProxyHandler(cfg *config.Config, modifiers map[string]RequestModifier) req.Header = cleanHeaders - log.Printf("[proxy] %s -> %s%s", r.URL.Path, req.URL.Host, req.URL.Path) + logger.Debug("%s -> %s%s", r.URL.Path, req.URL.Host, req.URL.Path) }, ModifyResponse: func(resp *http.Response) error { - log.Printf("[proxy] Response from %s%s: %d", resp.Request.URL.Host, resp.Request.URL.Path, resp.StatusCode) + logger.Debug("Response from %s%s: %d", resp.Request.URL.Host, resp.Request.URL.Path, resp.StatusCode) resp.Header.Del("Access-Control-Allow-Origin") // Avoid upstream conflicts return nil }, ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { - log.Printf("[proxy] Error proxying: %v", err) + logger.Error("Error proxying: %v", err) http.Error(rw, "Bad Gateway", http.StatusBadGateway) }, FlushInterval: -1, // immediate flush for SSE } if isSSE { + // Add special response handling for SSE connections to rewrite endpoint URLs + rp.Transport = &sseTransport{ + Transport: http.DefaultTransport, + proxyHost: r.Host, + targetHost: targetURL.Host, + } + + // Set SSE-specific headers + w.Header().Set("X-Accel-Buffering", "no") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + // Keep SSE connections open HandleSSE(w, r, rp) } else { @@ -236,6 +253,7 @@ func getAllowedOrigin(origin string, cfg *config.Config) string { return cfg.CORSConfig.AllowedOrigins[0] // Default to first allowed origin } for _, allowed := range cfg.CORSConfig.AllowedOrigins { + logger.Debug("Checking CORS origin: %s against allowed: %s", origin, allowed) if allowed == origin { return allowed } @@ -256,6 +274,7 @@ func addCORSHeaders(w http.ResponseWriter, cfg *config.Config, allowedOrigin, re w.Header().Set("Access-Control-Allow-Credentials", "true") } w.Header().Set("Vary", "Origin") + w.Header().Set("X-Accel-Buffering", "no") } func isAuthPath(path string) bool { @@ -273,7 +292,8 @@ func isAuthPath(path string) bool { // isMCPPath checks if the path is an MCP path func isMCPPath(path string, cfg *config.Config) bool { - for _, p := range cfg.MCPPaths { + mcpPaths := cfg.GetMCPPaths() + for _, p := range mcpPaths { if strings.HasPrefix(path, p) { return true } diff --git a/internal/proxy/sse.go b/internal/proxy/sse.go index 44d6558..ce72e04 100644 --- a/internal/proxy/sse.go +++ b/internal/proxy/sse.go @@ -1,11 +1,16 @@ package proxy import ( + "bufio" "context" - "log" + "fmt" + "io" "net/http" "net/http/httputil" + "strings" "time" + + "github.com/wso2/open-mcp-auth-proxy/internal/logging" ) // HandleSSE sets up a go-routine to wait for context cancellation @@ -16,7 +21,7 @@ func HandleSSE(w http.ResponseWriter, r *http.Request, rp *httputil.ReverseProxy go func() { <-ctx.Done() - log.Printf("INFO: SSE connection closed from %s (path: %s)", r.RemoteAddr, r.URL.Path) + logger.Info("SSE connection closed from %s (path: %s)", r.RemoteAddr, r.URL.Path) close(done) }() @@ -32,3 +37,73 @@ func HandleSSE(w http.ResponseWriter, r *http.Request, rp *httputil.ReverseProxy func NewShutdownContext(timeout time.Duration) (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), timeout) } + +// sseTransport is a custom http.RoundTripper that intercepts and modifies SSE responses +type sseTransport struct { + Transport http.RoundTripper + proxyHost string + targetHost string +} + +func (t *sseTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Call the underlying transport + resp, err := t.Transport.RoundTrip(req) + if err != nil { + return nil, err + } + + // Check if this is an SSE response + contentType := resp.Header.Get("Content-Type") + if !strings.Contains(contentType, "text/event-stream") { + return resp, nil + } + + logger.Info("Intercepting SSE response to modify endpoint events") + + // Create a response wrapper that modifies the response body + originalBody := resp.Body + pr, pw := io.Pipe() + + go func() { + defer originalBody.Close() + defer pw.Close() + + scanner := bufio.NewScanner(originalBody) + for scanner.Scan() { + line := scanner.Text() + + // Check if this line contains an endpoint event + if strings.HasPrefix(line, "event: endpoint") { + // Read the data line + if scanner.Scan() { + dataLine := scanner.Text() + if strings.HasPrefix(dataLine, "data: ") { + // Extract the endpoint URL + endpoint := strings.TrimPrefix(dataLine, "data: ") + + // Replace the host in the endpoint + logger.Debug("Original endpoint: %s", endpoint) + endpoint = strings.Replace(endpoint, t.targetHost, t.proxyHost, 1) + logger.Debug("Modified endpoint: %s", endpoint) + + // Write the modified event lines + fmt.Fprintln(pw, line) + fmt.Fprintln(pw, "data: "+endpoint) + continue + } + } + } + + // Write the original line for non-endpoint events + fmt.Fprintln(pw, line) + } + + if err := scanner.Err(); err != nil { + logger.Error("Error reading SSE stream: %v", err) + } + }() + + // Replace the response body with our modified pipe + resp.Body = pr + return resp, nil +} diff --git a/internal/subprocess/manager.go b/internal/subprocess/manager.go new file mode 100644 index 0000000..fa64337 --- /dev/null +++ b/internal/subprocess/manager.go @@ -0,0 +1,268 @@ +package subprocess + +import ( + "fmt" + "os" + "os/exec" + "sync" + "syscall" + "time" + "strings" + + "github.com/wso2/open-mcp-auth-proxy/internal/config" + "github.com/wso2/open-mcp-auth-proxy/internal/logging" +) + +// Manager handles starting and graceful shutdown of subprocesses +type Manager struct { + process *os.Process + processGroup int + mutex sync.Mutex + cmd *exec.Cmd + shutdownDelay time.Duration +} + +// NewManager creates a new subprocess manager +func NewManager() *Manager { + return &Manager{ + shutdownDelay: 5 * time.Second, + } +} + +// EnsureDependenciesAvailable checks and installs required package executors +func EnsureDependenciesAvailable(command string) error { + // Always ensure npx is available regardless of the command + if _, err := exec.LookPath("npx"); err != nil { + // npx is not available, check if npm is installed + if _, err := exec.LookPath("npm"); err != nil { + return fmt.Errorf("npx not found and npm not available; please install Node.js from https://nodejs.org/") + } + + // Try to install npx using npm + logger.Info("npx not found, attempting to install...") + cmd := exec.Command("npm", "install", "-g", "npx") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to install npx: %w", err) + } + + logger.Info("npx installed successfully") + } + + // Check if uv is needed based on the command + if strings.Contains(command, "uv ") { + if _, err := exec.LookPath("uv"); err != nil { + return fmt.Errorf("command requires uv but it's not installed; please install it following instructions at https://github.com/astral-sh/uv") + } + } + + return nil +} + +// SetShutdownDelay sets the maximum time to wait for graceful shutdown +func (m *Manager) SetShutdownDelay(duration time.Duration) { + m.shutdownDelay = duration +} + +// Start launches a subprocess based on the configuration +func (m *Manager) Start(cfg *config.Config) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + // If a process is already running, return an error + if m.process != nil { + return os.ErrExist + } + + if !cfg.Stdio.Enabled || cfg.Stdio.UserCommand == "" { + return nil // Nothing to start + } + + // Get the full command string + execCommand := cfg.BuildExecCommand() + if execCommand == "" { + return nil // No command to execute + } + + logger.Info("Starting subprocess with command: %s", execCommand) + + // Use the shell to execute the command + cmd := exec.Command("sh", "-c", execCommand) + + // Set working directory if specified + if cfg.Stdio.WorkDir != "" { + cmd.Dir = cfg.Stdio.WorkDir + } + + // Set environment variables if specified + if len(cfg.Stdio.Env) > 0 { + cmd.Env = append(os.Environ(), cfg.Stdio.Env...) + } + + // Capture stdout/stderr + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Set the process group for proper termination + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + // Start the process + if err := cmd.Start(); err != nil { + return err + } + + m.process = cmd.Process + m.cmd = cmd + logger.Info("Subprocess started with PID: %d", m.process.Pid) + + // Get and store the process group ID + pgid, err := syscall.Getpgid(m.process.Pid) + if err == nil { + m.processGroup = pgid + logger.Debug("Process group ID: %d", m.processGroup) + } else { + logger.Warn("Failed to get process group ID: %v", err) + m.processGroup = m.process.Pid + } + + // Handle process termination in background + go func() { + if err := cmd.Wait(); err != nil { + logger.Error("Subprocess exited with error: %v", err) + } else { + logger.Info("Subprocess exited successfully") + } + + // Clear the process reference when it exits + m.mutex.Lock() + m.process = nil + m.cmd = nil + m.mutex.Unlock() + }() + + return nil +} + +// IsRunning checks if the subprocess is running +func (m *Manager) IsRunning() bool { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.process != nil +} + +// Shutdown gracefully terminates the subprocess +func (m *Manager) Shutdown() { + m.mutex.Lock() + processToTerminate := m.process // Local copy of the process reference + processGroupToTerminate := m.processGroup + m.mutex.Unlock() + + if processToTerminate == nil { + return // No process to terminate + } + + logger.Info("Terminating subprocess...") + terminateComplete := make(chan struct{}) + + go func() { + defer close(terminateComplete) + + // Try graceful termination first with SIGTERM + terminatedGracefully := false + + // Try to terminate the process group first + if processGroupToTerminate != 0 { + err := syscall.Kill(-processGroupToTerminate, syscall.SIGTERM) + if err != nil { + logger.Warn("Failed to send SIGTERM to process group: %v", err) + + // Fallback to terminating just the process + m.mutex.Lock() + if m.process != nil { + err = m.process.Signal(syscall.SIGTERM) + if err != nil { + logger.Warn("Failed to send SIGTERM to process: %v", err) + } + } + m.mutex.Unlock() + } + } else { + // Try to terminate just the process + m.mutex.Lock() + if m.process != nil { + err := m.process.Signal(syscall.SIGTERM) + if err != nil { + logger.Warn("Failed to send SIGTERM to process: %v", err) + } + } + m.mutex.Unlock() + } + + // Wait for the process to exit gracefully + for i := 0; i < 10; i++ { + time.Sleep(200 * time.Millisecond) + + m.mutex.Lock() + if m.process == nil { + terminatedGracefully = true + m.mutex.Unlock() + break + } + m.mutex.Unlock() + } + + if terminatedGracefully { + logger.Info("Subprocess terminated gracefully") + return + } + + // If the process didn't exit gracefully, force kill + logger.Warn("Subprocess didn't exit gracefully, forcing termination...") + + // Try to kill the process group first + if processGroupToTerminate != 0 { + if err := syscall.Kill(-processGroupToTerminate, syscall.SIGKILL); err != nil { + logger.Warn("Failed to send SIGKILL to process group: %v", err) + + // Fallback to killing just the process + m.mutex.Lock() + if m.process != nil { + if err := m.process.Kill(); err != nil { + logger.Error("Failed to kill process: %v", err) + } + } + m.mutex.Unlock() + } + } else { + // Try to kill just the process + m.mutex.Lock() + if m.process != nil { + if err := m.process.Kill(); err != nil { + logger.Error("Failed to kill process: %v", err) + } + } + m.mutex.Unlock() + } + + // Wait a bit more to confirm termination + time.Sleep(500 * time.Millisecond) + + m.mutex.Lock() + if m.process == nil { + logger.Info("Subprocess terminated by force") + } else { + logger.Warn("Failed to terminate subprocess") + } + m.mutex.Unlock() + }() + + // Wait for termination with timeout + select { + case <-terminateComplete: + // Termination completed + case <-time.After(m.shutdownDelay): + logger.Warn("Subprocess termination timed out") + } +} diff --git a/internal/util/jwks.go b/internal/util/jwks.go index 4832bf8..f80d82e 100644 --- a/internal/util/jwks.go +++ b/internal/util/jwks.go @@ -4,12 +4,12 @@ import ( "crypto/rsa" "encoding/json" "errors" - "log" "math/big" "net/http" "strings" "github.com/golang-jwt/jwt/v4" + "github.com/wso2/open-mcp-auth-proxy/internal/logging" ) type JWKS struct { @@ -50,7 +50,7 @@ func FetchJWKS(jwksURL string) error { publicKeys[parsedKey.Kid] = pubKey } } - log.Printf("[JWKS] Loaded %d public keys.", len(publicKeys)) + logger.Info("Loaded %d public keys.", len(publicKeys)) return nil } From b2b2124b76cd4648a4af38745736318ccb7fc3f3 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Tue, 8 Apr 2025 13:26:16 +0530 Subject: [PATCH 20/40] Add unit tests --- .github/workflows/go.yml | 62 ++++++++++ .gitignore | 4 + .vscode/settings.json | 4 + Makefile | 74 ++++++++++++ internal/authz/default_test.go | 125 ++++++++++++++++++++ internal/config/config_test.go | 196 ++++++++++++++++++++++++++++++++ internal/proxy/modifier_test.go | 148 ++++++++++++++++++++++++ internal/util/jwks_test.go | 143 +++++++++++++++++++++++ 8 files changed, 756 insertions(+) create mode 100644 .github/workflows/go.yml create mode 100644 .vscode/settings.json create mode 100644 Makefile create mode 100644 internal/authz/default_test.go create mode 100644 internal/config/config_test.go create mode 100644 internal/proxy/modifier_test.go create mode 100644 internal/util/jwks_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..a055e0d --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,62 @@ +name: Go CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.20', '1.21'] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + + - name: Get dependencies + run: go get -v -t -d ./... + + - name: Verify dependencies + run: go mod verify + + - name: Run go vet + run: go vet ./... + + - name: Run tests + run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage.txt + fail_ci_if_error: false + + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.20', '1.21'] + os: [ubuntu-latest, macos-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + + - name: Build + run: go build -v ./cmd/proxy diff --git a/.gitignore b/.gitignore index 2a2b503..8627e4b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ go.sum .DS_Store openmcpauthproxy + +# test out files +coverage.out +coverage.html diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..745c15c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "github.copilot.chat.codesearch.enabled": true, + "github.copilot.chat.newWorkspaceCreation.enabled": true +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..85d92f6 --- /dev/null +++ b/Makefile @@ -0,0 +1,74 @@ +# Makefile for open-mcp-auth-proxy + +# Variables +BINARY_NAME := openmcpauthproxy +GO := go +GOFMT := gofmt +GOVET := go vet +GOTEST := go test +GOLINT := golangci-lint +GOCOV := go tool cover +BUILD_DIR := build + +# Source files +SRC := $(shell find . -name "*.go" -not -path "./vendor/*") +PKGS := $(shell go list ./... | grep -v /vendor/) + +# Set build options +BUILD_OPTS := -v + +# Set test options +TEST_OPTS := -v -race + +.PHONY: all build clean test fmt lint vet coverage help + +# Default target +all: lint test build + +# Build the application +build: + @echo "Building $(BINARY_NAME)..." + @mkdir -p $(BUILD_DIR) + $(GO) build $(BUILD_OPTS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/proxy + +# Clean build artifacts +clean: + @echo "Cleaning build artifacts..." + @rm -rf $(BUILD_DIR) + @rm -f coverage.out + +# Run tests +test: + @echo "Running tests..." + $(GOTEST) $(TEST_OPTS) ./... + +# Run tests with coverage report +coverage: + @echo "Running tests with coverage..." + @$(GOTEST) -coverprofile=coverage.out ./... + @$(GOCOV) -func=coverage.out + @$(GOCOV) -html=coverage.out -o coverage.html + @echo "Coverage report generated in coverage.html" + +# Run gofmt +fmt: + @echo "Running gofmt..." + @$(GOFMT) -w -s $(SRC) + +# Run go vet +vet: + @echo "Running go vet..." + @$(GOVET) ./... + +# Show help +help: + @echo "Available targets:" + @echo " all : Run lint, test, and build" + @echo " build : Build the application" + @echo " clean : Clean build artifacts" + @echo " test : Run tests" + @echo " coverage : Run tests with coverage report" + @echo " fmt : Run gofmt" + @echo " lint : Run golangci-lint" + @echo " vet : Run go vet" + @echo " help : Show this help message" diff --git a/internal/authz/default_test.go b/internal/authz/default_test.go new file mode 100644 index 0000000..f40030f --- /dev/null +++ b/internal/authz/default_test.go @@ -0,0 +1,125 @@ +package authz + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/wso2/open-mcp-auth-proxy/internal/config" +) + +func TestNewDefaultProvider(t *testing.T) { + cfg := &config.Config{} + provider := NewDefaultProvider(cfg) + + if provider == nil { + t.Fatal("Expected non-nil provider") + } + + // Ensure it implements the Provider interface + var _ Provider = provider +} + +func TestDefaultProviderWellKnownHandler(t *testing.T) { + // Create a config with a custom well-known response + cfg := &config.Config{ + Default: config.DefaultConfig{ + Path: map[string]config.PathConfig{ + "/.well-known/oauth-authorization-server": { + Response: &config.ResponseConfig{ + Issuer: "https://test-issuer.com", + JwksURI: "https://test-issuer.com/jwks", + ResponseTypesSupported: []string{"code"}, + GrantTypesSupported: []string{"authorization_code"}, + CodeChallengeMethodsSupported: []string{"S256"}, + }, + }, + }, + }, + } + + provider := NewDefaultProvider(cfg) + handler := provider.WellKnownHandler() + + // Create a test request + req := httptest.NewRequest("GET", "/.well-known/oauth-authorization-server", nil) + req.Host = "test-host.com" + req.Header.Set("X-Forwarded-Proto", "https") + + // Create a response recorder + w := httptest.NewRecorder() + + // Call the handler + handler(w, req) + + // Check response status + if w.Code != http.StatusOK { + t.Errorf("Expected status OK, got %v", w.Code) + } + + // Verify content type + contentType := w.Header().Get("Content-Type") + if contentType != "application/json" { + t.Errorf("Expected Content-Type: application/json, got %s", contentType) + } + + // Decode and check the response body + var response map[string]interface{} + if err := json.NewDecoder(w.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode response JSON: %v", err) + } + + // Check expected values + if response["issuer"] != "https://test-issuer.com" { + t.Errorf("Expected issuer=https://test-issuer.com, got %v", response["issuer"]) + } + if response["jwks_uri"] != "https://test-issuer.com/jwks" { + t.Errorf("Expected jwks_uri=https://test-issuer.com/jwks, got %v", response["jwks_uri"]) + } + if response["authorization_endpoint"] != "https://test-host.com/authorize" { + t.Errorf("Expected authorization_endpoint=https://test-host.com/authorize, got %v", response["authorization_endpoint"]) + } +} + +func TestDefaultProviderHandleOPTIONS(t *testing.T) { + provider := NewDefaultProvider(&config.Config{}) + handler := provider.WellKnownHandler() + + // Create OPTIONS request + req := httptest.NewRequest("OPTIONS", "/.well-known/oauth-authorization-server", nil) + w := httptest.NewRecorder() + + // Call the handler + handler(w, req) + + // Check response + if w.Code != http.StatusNoContent { + t.Errorf("Expected status NoContent for OPTIONS request, got %v", w.Code) + } + + // Check CORS headers + if w.Header().Get("Access-Control-Allow-Origin") != "*" { + t.Errorf("Expected Access-Control-Allow-Origin: *, got %s", w.Header().Get("Access-Control-Allow-Origin")) + } + if w.Header().Get("Access-Control-Allow-Methods") != "GET, OPTIONS" { + t.Errorf("Expected Access-Control-Allow-Methods: GET, OPTIONS, got %s", w.Header().Get("Access-Control-Allow-Methods")) + } +} + +func TestDefaultProviderInvalidMethod(t *testing.T) { + provider := NewDefaultProvider(&config.Config{}) + handler := provider.WellKnownHandler() + + // Create POST request (which should be rejected) + req := httptest.NewRequest("POST", "/.well-known/oauth-authorization-server", nil) + w := httptest.NewRecorder() + + // Call the handler + handler(w, req) + + // Check response + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("Expected status MethodNotAllowed for POST request, got %v", w.Code) + } +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..20c0893 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,196 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadConfig(t *testing.T) { + // Create a temporary config file + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "test_config.yaml") + + // Basic valid config + validConfig := ` +listen_port: 8080 +base_url: "http://localhost:8000" +transport_mode: "sse" +paths: + sse: "/sse" + messages: "/messages" +cors: + allowed_origins: + - "http://localhost:5173" + allowed_methods: + - "GET" + - "POST" + allowed_headers: + - "Authorization" + - "Content-Type" + allow_credentials: true +` + err := os.WriteFile(configPath, []byte(validConfig), 0644) + if err != nil { + t.Fatalf("Failed to create test config file: %v", err) + } + + // Test loading the valid config + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("Failed to load valid config: %v", err) + } + + // Verify expected values from the config + if cfg.ListenPort != 8080 { + t.Errorf("Expected ListenPort=8080, got %d", cfg.ListenPort) + } + if cfg.BaseURL != "http://localhost:8000" { + t.Errorf("Expected BaseURL=http://localhost:8000, got %s", cfg.BaseURL) + } + if cfg.TransportMode != SSETransport { + t.Errorf("Expected TransportMode=sse, got %s", cfg.TransportMode) + } + if cfg.Paths.SSE != "/sse" { + t.Errorf("Expected Paths.SSE=/sse, got %s", cfg.Paths.SSE) + } + if cfg.Paths.Messages != "/messages" { + t.Errorf("Expected Paths.Messages=/messages, got %s", cfg.Paths.Messages) + } + + // Test default values + if cfg.TimeoutSeconds != 15 { + t.Errorf("Expected default TimeoutSeconds=15, got %d", cfg.TimeoutSeconds) + } + if cfg.Port != 8000 { + t.Errorf("Expected default Port=8000, got %d", cfg.Port) + } +} + +func TestValidate(t *testing.T) { + tests := []struct { + name string + config Config + expectError bool + }{ + { + name: "Valid SSE config", + config: Config{ + TransportMode: SSETransport, + Paths: PathsConfig{ + SSE: "/sse", + Messages: "/messages", + }, + BaseURL: "http://localhost:8000", + }, + expectError: false, + }, + { + name: "Valid stdio config", + config: Config{ + TransportMode: StdioTransport, + Stdio: StdioConfig{ + Enabled: true, + UserCommand: "some-command", + }, + }, + expectError: false, + }, + { + name: "Invalid stdio config - not enabled", + config: Config{ + TransportMode: StdioTransport, + Stdio: StdioConfig{ + Enabled: false, + UserCommand: "some-command", + }, + }, + expectError: true, + }, + { + name: "Invalid stdio config - no command", + config: Config{ + TransportMode: StdioTransport, + Stdio: StdioConfig{ + Enabled: true, + UserCommand: "", + }, + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.config.Validate() + if tc.expectError && err == nil { + t.Errorf("Expected validation error but got none") + } + if !tc.expectError && err != nil { + t.Errorf("Expected no validation error but got: %v", err) + } + }) + } +} + +func TestGetMCPPaths(t *testing.T) { + cfg := Config{ + Paths: PathsConfig{ + SSE: "/custom-sse", + Messages: "/custom-messages", + }, + } + + paths := cfg.GetMCPPaths() + if len(paths) != 2 { + t.Errorf("Expected 2 MCP paths, got %d", len(paths)) + } + if paths[0] != "/custom-sse" { + t.Errorf("Expected first path=/custom-sse, got %s", paths[0]) + } + if paths[1] != "/custom-messages" { + t.Errorf("Expected second path=/custom-messages, got %s", paths[1]) + } +} + +func TestBuildExecCommand(t *testing.T) { + tests := []struct { + name string + config Config + expectedResult string + }{ + { + name: "Valid command", + config: Config{ + Stdio: StdioConfig{ + UserCommand: "test-command", + }, + Port: 8080, + BaseURL: "http://example.com", + Paths: PathsConfig{ + SSE: "/sse-path", + Messages: "/msgs", + }, + }, + expectedResult: `npx -y supergateway --stdio "test-command" --port 8080 --baseUrl http://example.com --ssePath /sse-path --messagePath /msgs`, + }, + { + name: "Empty command", + config: Config{ + Stdio: StdioConfig{ + UserCommand: "", + }, + }, + expectedResult: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.config.BuildExecCommand() + if result != tc.expectedResult { + t.Errorf("Expected command=%s, got %s", tc.expectedResult, result) + } + }) + } +} diff --git a/internal/proxy/modifier_test.go b/internal/proxy/modifier_test.go new file mode 100644 index 0000000..6ec7aca --- /dev/null +++ b/internal/proxy/modifier_test.go @@ -0,0 +1,148 @@ +package proxy + +import ( + "net/http" + "net/url" + "strings" + "testing" + + "github.com/wso2/open-mcp-auth-proxy/internal/config" +) + +func TestAuthorizationModifier(t *testing.T) { + cfg := &config.Config{ + Default: config.DefaultConfig{ + Path: map[string]config.PathConfig{ + "/authorize": { + AddQueryParams: []config.ParamConfig{ + {Name: "client_id", Value: "test-client-id"}, + {Name: "scope", Value: "openid"}, + }, + }, + }, + }, + } + + modifier := &AuthorizationModifier{Config: cfg} + + // Create a test request + req, err := http.NewRequest("GET", "/authorize?response_type=code", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Modify the request + modifiedReq, err := modifier.ModifyRequest(req) + if err != nil { + t.Fatalf("ModifyRequest failed: %v", err) + } + + // Check that the query parameters were added + query := modifiedReq.URL.Query() + if query.Get("client_id") != "test-client-id" { + t.Errorf("Expected client_id=test-client-id, got %s", query.Get("client_id")) + } + if query.Get("scope") != "openid" { + t.Errorf("Expected scope=openid, got %s", query.Get("scope")) + } + if query.Get("response_type") != "code" { + t.Errorf("Expected response_type=code, got %s", query.Get("response_type")) + } +} + +func TestTokenModifier(t *testing.T) { + cfg := &config.Config{ + Default: config.DefaultConfig{ + Path: map[string]config.PathConfig{ + "/token": { + AddBodyParams: []config.ParamConfig{ + {Name: "audience", Value: "test-audience"}, + }, + }, + }, + }, + } + + modifier := &TokenModifier{Config: cfg} + + // Create a test request with form data + form := url.Values{} + + req, err := http.NewRequest("POST", "/token", strings.NewReader(form.Encode())) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + // Modify the request + modifiedReq, err := modifier.ModifyRequest(req) + if err != nil { + t.Fatalf("ModifyRequest failed: %v", err) + } + + body := make([]byte, 1024) + n, err := modifiedReq.Body.Read(body) + if err != nil && err.Error() != "EOF" { + t.Fatalf("Failed to read body: %v", err) + } + bodyStr := string(body[:n]) + print(bodyStr) + + // Parse the form data from the modified request + if err := modifiedReq.ParseForm(); err != nil { + t.Fatalf("Failed to parse form data: %v", err) + } + + // Check that the body parameters were added + if !strings.Contains(bodyStr, "audience") { + t.Errorf("Expected body to contain audience, got %s", bodyStr) + } +} + +func TestRegisterModifier(t *testing.T) { + cfg := &config.Config{ + Default: config.DefaultConfig{ + Path: map[string]config.PathConfig{ + "/register": { + AddBodyParams: []config.ParamConfig{ + {Name: "client_name", Value: "test-client"}, + }, + }, + }, + }, + } + + modifier := &RegisterModifier{Config: cfg} + + // Create a test request with JSON data + jsonBody := `{"redirect_uris":["https://example.com/callback"]}` + req, err := http.NewRequest("POST", "/register", strings.NewReader(jsonBody)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + // Modify the request + modifiedReq, err := modifier.ModifyRequest(req) + if err != nil { + t.Fatalf("ModifyRequest failed: %v", err) + } + + // Read the body and check that it still contains the original data + // This test would need to be enhanced with a proper JSON parsing to verify + // the added parameters + body := make([]byte, 1024) + n, err := modifiedReq.Body.Read(body) + if err != nil && err.Error() != "EOF" { + t.Fatalf("Failed to read body: %v", err) + } + bodyStr := string(body[:n]) + + // Simple check to see if the modified body contains the expected fields + if !strings.Contains(bodyStr, "client_name") { + t.Errorf("Expected body to contain client_name, got %s", bodyStr) + } + if !strings.Contains(bodyStr, "redirect_uris") { + t.Errorf("Expected body to contain redirect_uris, got %s", bodyStr) + } +} diff --git a/internal/util/jwks_test.go b/internal/util/jwks_test.go new file mode 100644 index 0000000..3b00c68 --- /dev/null +++ b/internal/util/jwks_test.go @@ -0,0 +1,143 @@ +package util + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +func TestValidateJWT(t *testing.T) { + // Initialize the test JWKS data + initTestJWKS(t) + + // Test cases + tests := []struct { + name string + authHeader string + expectError bool + }{ + { + name: "Valid JWT token", + authHeader: "Bearer " + createValidJWT(t), + expectError: false, + }, + { + name: "No auth header", + authHeader: "", + expectError: true, + }, + { + name: "Invalid auth header format", + authHeader: "InvalidFormat", + expectError: true, + }, + { + name: "Invalid JWT token", + authHeader: "Bearer invalid.jwt.token", + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := ValidateJWT(tc.authHeader) + if tc.expectError && err == nil { + t.Errorf("Expected error but got none") + } + if !tc.expectError && err != nil { + t.Errorf("Expected no error but got: %v", err) + } + }) + } +} + +func TestFetchJWKS(t *testing.T) { + // Create a mock JWKS server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Generate a test RSA key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Failed to generate RSA key: %v", err) + } + + // Create JWKS response + jwks := map[string]interface{}{ + "keys": []map[string]interface{}{ + { + "kty": "RSA", + "kid": "test-key-id", + "n": base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()), + "e": base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}), // Default exponent 65537 + }, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(jwks) + })) + defer server.Close() + + // Test fetching JWKS + err := FetchJWKS(server.URL) + if err != nil { + t.Fatalf("FetchJWKS failed: %v", err) + } + + // Check that keys were stored + if len(publicKeys) == 0 { + t.Errorf("Expected publicKeys to be populated") + } +} + +// Helper function to initialize test JWKS data +func initTestJWKS(t *testing.T) { + // Create a test RSA key pair + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Failed to generate RSA key: %v", err) + } + + // Initialize the publicKeys map + publicKeys = map[string]*rsa.PublicKey{ + "test-key-id": &privateKey.PublicKey, + } +} + +// Helper function to create a valid JWT token for testing +func createValidJWT(t *testing.T) string { + // Create a test RSA key pair + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Failed to generate RSA key: %v", err) + } + + // Ensure the test key is in the publicKeys map + if publicKeys == nil { + publicKeys = map[string]*rsa.PublicKey{} + } + publicKeys["test-key-id"] = &privateKey.PublicKey + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ + "sub": "1234567890", + "name": "Test User", + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), + }) + token.Header["kid"] = "test-key-id" + + // Sign the token + tokenString, err := token.SignedString(privateKey) + if err != nil { + t.Fatalf("Failed to sign token: %v", err) + } + + return tokenString +} From 9a3d5346f2f89d9219091330e85adae083e93d77 Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath <43197743+shashimalcse@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:32:07 +0530 Subject: [PATCH 21/40] Fix Auth0 configs (#15) --- docs/Auth0.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/Auth0.md b/docs/Auth0.md index fe55edc..ef1b554 100644 --- a/docs/Auth0.md +++ b/docs/Auth0.md @@ -28,9 +28,17 @@ Update your `config.yaml` with Auth0 settings: ```yaml # Basic proxy configuration -mcp_server_base_url: "http://localhost:8000" listen_port: 8080 -timeout_seconds: 10 +base_url: "http://localhost:8000" +port: 8000 + +# Path configuration +paths: + sse: "/sse" + messages: "/messages/" + +# Transport mode +transport_mode: "sse" # CORS configuration cors: From aa5b32aa8a69eb5e92547659e7433150f99a90b9 Mon Sep 17 00:00:00 2001 From: Omindu Rathnaweera Date: Tue, 8 Apr 2025 21:40:08 +0530 Subject: [PATCH 22/40] Create pr-builder.yml --- .github/workflows/pr-builder.yml | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/pr-builder.yml diff --git a/.github/workflows/pr-builder.yml b/.github/workflows/pr-builder.yml new file mode 100644 index 0000000..d130d39 --- /dev/null +++ b/.github/workflows/pr-builder.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '1.16' + + - name: Cache Go modules + uses: actions/cache@v2 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install dependencies + run: go mod tidy + + - name: Build + run: go build -v ./... + + - name: Run tests + run: go test -v ./... + + - name: Build status + run: echo "Build succeeded!" From 8f57cfe3e0cdcab84b324857b247cd74bc7d6484 Mon Sep 17 00:00:00 2001 From: Omindu Rathnaweera Date: Tue, 8 Apr 2025 21:45:11 +0530 Subject: [PATCH 23/40] Update pr-builder.yml --- .github/workflows/pr-builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-builder.yml b/.github/workflows/pr-builder.yml index d130d39..b0cea6e 100644 --- a/.github/workflows/pr-builder.yml +++ b/.github/workflows/pr-builder.yml @@ -1,4 +1,4 @@ -name: CI +name: pr-builder.yml on: pull_request: From 9ecaabecd2267e4f4e32f876fcc51f65a89e0d2a Mon Sep 17 00:00:00 2001 From: Thilina Shashimal Senarath Date: Wed, 9 Apr 2025 09:52:40 +0530 Subject: [PATCH 24/40] Remove vscode files --- .gitignore | 4 ++++ .vscode/settings.json | 4 ---- Makefile | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 8627e4b..4ceafef 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,12 @@ go.sum # OS generated files .DS_Store +# builds openmcpauthproxy # test out files coverage.out coverage.html + +# IDE files +.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 745c15c..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "github.copilot.chat.codesearch.enabled": true, - "github.copilot.chat.newWorkspaceCreation.enabled": true -} diff --git a/Makefile b/Makefile index 85d92f6..7fd2574 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,5 @@ help: @echo " test : Run tests" @echo " coverage : Run tests with coverage report" @echo " fmt : Run gofmt" - @echo " lint : Run golangci-lint" @echo " vet : Run go vet" @echo " help : Show this help message" From aa7f76a548ed1d922978cd631e2a7337b57a49d2 Mon Sep 17 00:00:00 2001 From: Omindu Rathnaweera Date: Sat, 12 Apr 2025 10:55:31 +0530 Subject: [PATCH 25/40] Update pr-builder.yml --- .github/workflows/pr-builder.yml | 67 +++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pr-builder.yml b/.github/workflows/pr-builder.yml index b0cea6e..a055e0d 100644 --- a/.github/workflows/pr-builder.yml +++ b/.github/workflows/pr-builder.yml @@ -1,41 +1,62 @@ -name: pr-builder.yml +name: Go CI on: + push: + branches: [ main ] pull_request: - branches: - - main + branches: [ main ] jobs: - build: + test: + name: Test runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.20', '1.21'] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: '1.16' + go-version: ${{ matrix.go-version }} - - name: Cache Go modules - uses: actions/cache@v2 - with: - path: | - ~/go/pkg/mod - ~/.cache/go-build - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + - name: Get dependencies + run: go get -v -t -d ./... - - name: Install dependencies - run: go mod tidy + - name: Verify dependencies + run: go mod verify - - name: Build - run: go build -v ./... + - name: Run go vet + run: go vet ./... - name: Run tests - run: go test -v ./... + run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... - - name: Build status - run: echo "Build succeeded!" + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage.txt + fail_ci_if_error: false + + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.20', '1.21'] + os: [ubuntu-latest, macos-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + + - name: Build + run: go build -v ./cmd/proxy From b32f25e6947ccf1d14c331e615b3782742e34296 Mon Sep 17 00:00:00 2001 From: Omindu Rathnaweera Date: Sat, 12 Apr 2025 10:56:10 +0530 Subject: [PATCH 26/40] Delete .github/workflows/go.yml --- .github/workflows/go.yml | 62 ---------------------------------------- 1 file changed, 62 deletions(-) delete mode 100644 .github/workflows/go.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index a055e0d..0000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Go CI - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test: - name: Test - runs-on: ubuntu-latest - strategy: - matrix: - go-version: ['1.20', '1.21'] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ matrix.go-version }} - - - name: Get dependencies - run: go get -v -t -d ./... - - - name: Verify dependencies - run: go mod verify - - - name: Run go vet - run: go vet ./... - - - name: Run tests - run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: ./coverage.txt - fail_ci_if_error: false - - build: - name: Build - runs-on: ubuntu-latest - strategy: - matrix: - go-version: ['1.20', '1.21'] - os: [ubuntu-latest, macos-latest] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ matrix.go-version }} - - - name: Build - run: go build -v ./cmd/proxy From ecee345f9c0acf50bf15e3e7f52c61cb21d05f35 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Tue, 15 Apr 2025 08:53:53 +0530 Subject: [PATCH 27/40] Add guide for Keycloak integration --- config.yaml | 1 + docs/{ => integrations}/Auth0.md | 2 +- docs/integrations/keycloak.md | 92 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) rename docs/{ => integrations}/Auth0.md (97%) create mode 100644 docs/integrations/keycloak.md diff --git a/config.yaml b/config.yaml index 971b93c..5621195 100644 --- a/config.yaml +++ b/config.yaml @@ -37,6 +37,7 @@ cors: allowed_headers: - "Authorization" - "Content-Type" + - "mcp-protocol-version" allow_credentials: true # Demo configuration for Asgardeo diff --git a/docs/Auth0.md b/docs/integrations/Auth0.md similarity index 97% rename from docs/Auth0.md rename to docs/integrations/Auth0.md index ef1b554..9195659 100644 --- a/docs/Auth0.md +++ b/docs/integrations/Auth0.md @@ -4,7 +4,7 @@ This guide will help you configure Open MCP Auth Proxy to use Auth0 as your iden ### Prerequisites -- An Auth0 organization (sign up here if you don't have one) +- An Auth0 organization (sign up [here](https://auth0.com) if you don't have one) - Open MCP Auth Proxy installed ### Setting Up Auth0 diff --git a/docs/integrations/keycloak.md b/docs/integrations/keycloak.md new file mode 100644 index 0000000..a81e4d2 --- /dev/null +++ b/docs/integrations/keycloak.md @@ -0,0 +1,92 @@ +## Integrating Open MCP Auth Proxy with Keycloak + +This guide walks you through configuring the Open MCP Auth Proxy to authenticate using Keycloak as the identity provider. + +--- + +### Prerequisites + +Before you begin, ensure you have the following: + +- A running Keycloak instance +- Open MCP Auth Proxy installed and accessible + +--- + +### Step 1: Configure Keycloak for Client Registration + +Set up dynamic client registration in your Keycloak realm by following the [Keycloak client registration guide](https://www.keycloak.org/securing-apps/client-registration). + +--- + +### Step 2: Configure Open MCP Auth Proxy + +Update the `config.yaml` file in your Open MCP Auth Proxy setup using your Keycloak realm's [OIDC settings](https://www.keycloak.org/securing-apps/oidc-layers). Below is an example configuration: + +```yaml +# Proxy server configuration +listen_port: 8081 # Port for the auth proxy +base_url: "http://localhost:8000" # Base URL of the MCP server +port: 8000 # MCP server port + +# Define path mappings +paths: + sse: "/sse" + messages: "/messages/" + +# Set the transport mode +transport_mode: "sse" + +# CORS settings +cors: + allowed_origins: + - "http://localhost:5173" # Origin of your frontend/client app + allowed_methods: + - "GET" + - "POST" + - "PUT" + - "DELETE" + allowed_headers: + - "Authorization" + - "Content-Type" + - "mcp-protocol-version" + allow_credentials: true + +# Keycloak endpoint path mappings +path_mapping: + /token: /realms/master/protocol/openid-connect/token + /register: /realms/master/clients-registrations/openid-connect + +# Keycloak configuration block +default: + base_url: "http://localhost:8080" + jwks_url: "http://localhost:8080/realms/master/protocol/openid-connect/certs" + path: + /.well-known/oauth-authorization-server: + response: + issuer: "http://localhost:8080/realms/master" + jwks_uri: "http://localhost:8080/realms/master/protocol/openid-connect/certs" + authorization_endpoint: "http://localhost:8080/realms/master/protocol/openid-connect/auth" + response_types_supported: + - "code" + grant_types_supported: + - "authorization_code" + - "refresh_token" + code_challenge_methods_supported: + - "S256" + - "plain" + /token: + addBodyParams: + - name: "audience" + value: "mcp_proxy" +``` + +### Step 3: Start the Auth Proxy + +Launch the proxy with the updated Keycloak configuration: + +```bash +./openmcpauthproxy +``` + +Once running, the proxy will handle authentication requests through your configured Keycloak realm. From d3d2f336610618d9b42f5fc052a96757ec057711 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Tue, 15 Apr 2025 09:12:51 +0530 Subject: [PATCH 28/40] Improve formatting --- docs/integrations/keycloak.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/keycloak.md b/docs/integrations/keycloak.md index a81e4d2..5e338cc 100644 --- a/docs/integrations/keycloak.md +++ b/docs/integrations/keycloak.md @@ -25,9 +25,9 @@ Update the `config.yaml` file in your Open MCP Auth Proxy setup using your Keycl ```yaml # Proxy server configuration -listen_port: 8081 # Port for the auth proxy +listen_port: 8081 # Port for the auth proxy base_url: "http://localhost:8000" # Base URL of the MCP server -port: 8000 # MCP server port +port: 8000 # MCP server port # Define path mappings paths: From 42efe1f48a81a0a591773abf4eeaa82b6f1631dc Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Wed, 16 Apr 2025 12:08:31 +0530 Subject: [PATCH 29/40] Update internal/proxy/modifier_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/proxy/modifier_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/proxy/modifier_test.go b/internal/proxy/modifier_test.go index 6ec7aca..3d2fd44 100644 --- a/internal/proxy/modifier_test.go +++ b/internal/proxy/modifier_test.go @@ -86,7 +86,6 @@ func TestTokenModifier(t *testing.T) { t.Fatalf("Failed to read body: %v", err) } bodyStr := string(body[:n]) - print(bodyStr) // Parse the form data from the modified request if err := modifiedReq.ParseForm(); err != nil { From 6036ab30ecc6bb0e98dbab1bfca3e03b150927e5 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Thu, 17 Apr 2025 14:15:04 +0530 Subject: [PATCH 30/40] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f197891..f13d709 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ asgardeo: ### Other OAuth Providers -- [Auth0 Integration Guide](docs/Auth0.md) +- [Auth0](docs/integrations/Auth0.md) +- [Keycloak](docs/integrations/keycloak.md) ## Testing with an Example MCP Server @@ -226,4 +227,4 @@ asgardeo: org_name: "" client_id: "" client_secret: "" -``` \ No newline at end of file +``` From f4be3de30fa712f3b291ba68740a879b485df4f2 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 18 Apr 2025 15:12:32 +0530 Subject: [PATCH 31/40] Add release workflow (#23) * Add release workflow --- .github/scripts/release.sh | 124 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 64 ++++++++++++++++++ .gitignore | 5 +- Makefile | 29 ++++++-- go.mod | 2 +- go.sum | 6 ++ 6 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 .github/scripts/release.sh create mode 100644 .github/workflows/release.yml create mode 100644 go.sum diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh new file mode 100644 index 0000000..35568a2 --- /dev/null +++ b/.github/scripts/release.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +# Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). +# +# This software is the property of WSO2 LLC. and its suppliers, if any. +# Dissemination of any information or reproduction of any material contained +# herein in any form is strictly forbidden, unless permitted by WSO2 expressly. +# You may not alter or remove any copyright or other notice from copies of this content. +# + +# Exit the script on any command with non-zero exit status. +set -e +set -o pipefail + +UPSTREAM_BRANCH="main" + +# Assign command line arguments to variables. +GIT_TOKEN=$1 +WORK_DIR=$2 +VERSION_TYPE=$3 # possible values: major, minor, patch + + Check if GIT_TOKEN is empty +if [ -z "$GIT_TOKEN" ]; then + echo "❌ Error: GIT_TOKEN is not set." + exit 1 +fi + +# Check if WORK_DIR is empty +if [ -z "$WORK_DIR" ]; then + echo "❌ Error: WORK_DIR is not set." + exit 1 +fi + +# Validate VERSION_TYPE +if [[ "$VERSION_TYPE" != "major" && "$VERSION_TYPE" != "minor" && "$VERSION_TYPE" != "patch" ]]; then + echo "❌ Error: VERSION_TYPE must be one of: major, minor, or patch." + exit 1 +fi + +BUILD_DIRECTORY="$WORK_DIR/build" +RELEASE_DIRECTORY="$BUILD_DIRECTORY/releases" + +# Navigate to the working directory. +cd "${WORK_DIR}" + +# Create the release directory. +if [ ! -d "$RELEASE_DIRECTORY" ]; then + mkdir -p "$RELEASE_DIRECTORY" +else + rm -rf "$RELEASE_DIRECTORY"/* +fi + +# Extract current version. +CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0") +IFS='.' read -r MAJOR MINOR PATCH <<< "${CURRENT_VERSION}" + +# Determine which part to increment +case "$VERSION_TYPE" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch|*) + PATCH=$((PATCH + 1)) + ;; +esac + +NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" + +echo "Creating release packages for version $NEW_VERSION..." + +# List of supported OSes. +oses=("linux" "linux-arm" "darwin") + +# Navigate to the release directory. +cd "${RELEASE_DIRECTORY}" + +for os in "${oses[@]}"; do + os_dir="../$os" + + if [ -d "$os_dir" ]; then + release_artifact_folder="openmcpauthproxy_${os}-v${NEW_VERSION}" + mkdir -p "$release_artifact_folder" + + cp -r $os_dir/* "$release_artifact_folder" + + # Zip the release package. + zip_file="$release_artifact_folder.zip" + echo "Creating $zip_file..." + zip -r "$zip_file" "$release_artifact_folder" + + # Delete the folder after zipping. + rm -rf "$release_artifact_folder" + + # Generate checksum file. + sha256sum "$zip_file" | sed "s|target/releases/||" > "$zip_file.sha256" + echo "Checksum generated for the $os package." + + echo "Release packages created successfully for $os." + else + echo "Skipping $os release package creation as the build artifacts are not available." + fi +done + +echo "Release packages created successfully in $RELEASE_DIRECTORY." + +# Navigate back to the project root directory. +cd "${WORK_DIR}" + +# Collect all ZIP and .sha256 files in the target/releases directory. +FILES_TO_UPLOAD=$(find build/releases -type f \( -name "*.zip" -o -name "*.sha256" \)) + +# Create a release with the current version. +TAG_NAME="v${NEW_VERSION}" +export GITHUB_TOKEN="${GIT_TOKEN}" +gh release create "${TAG_NAME}" ${FILES_TO_UPLOAD} --title "${TAG_NAME}" --notes "OpenMCPAuthProxy - ${TAG_NAME}" --target "${UPSTREAM_BRANCH}" || { echo "Failed to create release"; exit 1; } + + +echo "Release ${TAG_NAME} created successfully." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e55f6b6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +# +# Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). +# +# This software is the property of WSO2 LLC. and its suppliers, if any. +# Dissemination of any information or reproduction of any material contained +# herein in any form is strictly forbidden, unless permitted by WSO2 expressly. +# You may not alter or remove any copyright or other notice from copies of this content. +# + +name: Release + +on: + workflow_dispatch: + inputs: + version_type: + type: choice + description: Choose the type of version update + options: + - 'major' + - 'minor' + - 'patch' + required: true + +jobs: + update-and-release: + runs-on: ubuntu-latest + env: + GOPROXY: https://proxy.golang.org + if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v2 + with: + ref: 'main' + fetch-depth: 0 + token: ${{ secrets.GIT_BOT_PAT }} + - uses: actions/checkout@v2 + + - name: Set up Go 1.x + uses: actions/setup-go@v3 + with: + go-version: "^1.x" + + - name: Cache Go modules + id: cache-go-modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-modules-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-modules- + + - name: Install dependencies + run: go mod download + + - name: Build and test + run: make build + working-directory: . + + - name: Update artifact version, package, commit, and create release. + env: + GITHUB_TOKEN: ${{ secrets.GIT_BOT_PAT }} + run: bash ./.github/scripts/release.sh $GITHUB_TOKEN ${{ github.workspace }} ${{ github.event.inputs.version_type }} diff --git a/.gitignore b/.gitignore index 4ceafef..d200b58 100644 --- a/.gitignore +++ b/.gitignore @@ -24,14 +24,11 @@ hs_err_pid* replay_pid* -# Go module cache files -go.sum - # OS generated files .DS_Store # builds -openmcpauthproxy +build # test out files coverage.out diff --git a/Makefile b/Makefile index 7fd2574..c9ef883 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ # Makefile for open-mcp-auth-proxy # Variables +PROJECT_ROOT := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))) BINARY_NAME := openmcpauthproxy GO := go GOFMT := gofmt @@ -20,16 +21,30 @@ BUILD_OPTS := -v # Set test options TEST_OPTS := -v -race -.PHONY: all build clean test fmt lint vet coverage help +.PHONY: all clean test fmt lint vet coverage help # Default target -all: lint test build +all: lint test build-linux build-linux-arm build-darwin -# Build the application -build: - @echo "Building $(BINARY_NAME)..." - @mkdir -p $(BUILD_DIR) - $(GO) build $(BUILD_OPTS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/proxy +build: clean test build-linux build-linux-arm build-darwin + +build-linux: + mkdir -p $(BUILD_DIR)/linux + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION) \ + -o $(BUILD_DIR)/linux/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy + cp config.yaml $(BUILD_DIR)/linux + +build-linux-arm: + mkdir -p $(BUILD_DIR)/linux-arm + GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION) \ + -o $(BUILD_DIR)/linux-arm/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy + cp config.yaml $(BUILD_DIR)/linux-arm + +build-darwin: + mkdir -p $(BUILD_DIR)/darwin + GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION) \ + -o $(BUILD_DIR)/darwin/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy + cp config.yaml $(BUILD_DIR)/darwin # Clean build artifacts clean: diff --git a/go.mod b/go.mod index 2d26216..0bceb4f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/wso2/open-mcp-auth-proxy -go 1.22.3 +go 1.21 require ( github.com/golang-jwt/jwt/v4 v4.5.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9d27ad1 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From 23c282dcfc57c0eb519101e5fb9eadde34e545f4 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 18 Apr 2025 21:40:12 +0530 Subject: [PATCH 32/40] Add badges to README (#25) --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f13d709..d01be7b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Open MCP Auth Proxy -A lightweight authorization proxy for Model Context Protocol (MCP) servers that enforces authorization according to the [MCP authorization specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/). +A lightweight authorization proxy for Model Context Protocol (MCP) servers that enforces authorization according to the [MCP authorization specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/) + +[![🚀 Release](https://github.com/wso2/open-mcp-auth-proxy/actions/workflows/release.yml/badge.svg)](https://github.com/wso2/open-mcp-auth-proxy/actions/workflows/release.yml) +[![💬 Stackoverflow](https://img.shields.io/badge/Ask%20for%20help%20on-Stackoverflow-orange)](https://stackoverflow.com/questions/tagged/wso2is) +[![💬 Discord](https://img.shields.io/badge/Join%20us%20on-Discord-%23e01563.svg)](https://discord.gg/wso2) +[![🐦 Twitter](https://img.shields.io/twitter/follow/wso2.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=wso2) +[![📝 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/wso2/product-is/blob/master/LICENSE) ![Architecture Diagram](https://github.com/user-attachments/assets/41cf6723-c488-4860-8640-8fec45006f92) From 5261a69f7a149f4755731754ebc538ef3348a7e5 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 18 Apr 2025 21:40:36 +0530 Subject: [PATCH 33/40] Improve ordering in README (#24) --- README.md | 78 +++++++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index d01be7b..71b4b60 100644 --- a/README.md +++ b/README.md @@ -25,17 +25,26 @@ Open MCP Auth Proxy sits between MCP clients and your MCP server to: * Go 1.20 or higher * A running MCP server + +> If you don't have an MCP server, you can use the included example: +> +> 1. Navigate to the `resources` directory +> 2. Set up a Python environment: +> +> ```bash +> python3 -m venv .venv +> source .venv/bin/activate +> pip3 install -r requirements.txt +> ``` +> +> 3. Start the example server: +> +> ```bash +> python3 echo_server.py +> ``` + * An MCP client that supports MCP authorization -### Installation - -```bash -git clone https://github.com/wso2/open-mcp-auth-proxy -cd open-mcp-auth-proxy -go get github.com/golang-jwt/jwt/v4 gopkg.in/yaml.v2 -go build -o openmcpauthproxy ./cmd/proxy -``` - ### Basic Usage 1. The repository comes with a default `config.yaml` file that contains the basic configuration: @@ -56,29 +65,18 @@ paths: 3. Connect using an MCP client like [MCP Inspector](https://github.com/shashimalcse/inspector)(This is a temporary fork with fixes for authentication [issues](https://github.com/modelcontextprotocol/typescript-sdk/issues/257) in the original implementation) -## Identity Provider Integration +## Connect an Identity Provider -### Demo Mode +### Asgardeo -For quick testing, use the `--demo` flag which includes pre-configured authentication and authorization with an Asgardeo sandbox. - -```bash -./openmcpauthproxy --demo -``` - -### Asgardeo Integration - -To enable authorization through your own Asgardeo organization: +To enable authorization through your Asgardeo organization: 1. [Register](https://asgardeo.io/signup) and create an organization in Asgardeo 2. Create an [M2M application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/) 1. [Authorize this application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/#authorize-the-api-resources-for-the-app) to invoke "Application Management API" with the `internal_application_mgt_create` scope ![image](https://github.com/user-attachments/assets/0bd57cac-1904-48cc-b7aa-0530224bc41a) - 2. Update the existing `config.yaml` with your Asgardeo details: - -#### Configure the Auth Proxy - -Create a configuration file config.yaml with the following parameters: + +3. Update `config.yaml` with the following parameters. ```yaml base_url: "http://localhost:8000" # URL of your MCP server @@ -90,7 +88,7 @@ asgardeo: client_secret: "" # Client secret of the M2M app ``` -3. Start the proxy with Asgardeo integration: +4. Start the proxy with Asgardeo integration: ```bash ./openmcpauthproxy --asgardeo @@ -101,25 +99,6 @@ asgardeo: - [Auth0](docs/integrations/Auth0.md) - [Keycloak](docs/integrations/keycloak.md) -## Testing with an Example MCP Server - -If you don't have an MCP server, you can use the included example: - -1. Navigate to the `resources` directory -2. Set up a Python environment: - -```bash -python3 -m venv .venv -source .venv/bin/activate -pip3 install -r requirements.txt -``` - -3. Start the example server: - -```bash -python3 echo_server.py -``` - # Advanced Configuration ### Transport Modes @@ -234,3 +213,12 @@ asgardeo: client_id: "" client_secret: "" ``` + +### Build from source + +```bash +git clone https://github.com/wso2/open-mcp-auth-proxy +cd open-mcp-auth-proxy +go get github.com/golang-jwt/jwt/v4 gopkg.in/yaml.v2 +go build -o openmcpauthproxy ./cmd/proxy +``` From 9ce9509cceb1b7fcb6f3b76118ca285a7320e5a0 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Mon, 21 Apr 2025 15:29:48 +0530 Subject: [PATCH 34/40] Fix issues in makefile (#26) --- .github/scripts/release.sh | 2 +- Makefile | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh index 35568a2..2a1f6a9 100644 --- a/.github/scripts/release.sh +++ b/.github/scripts/release.sh @@ -19,7 +19,7 @@ GIT_TOKEN=$1 WORK_DIR=$2 VERSION_TYPE=$3 # possible values: major, minor, patch - Check if GIT_TOKEN is empty +# Check if GIT_TOKEN is empty if [ -z "$GIT_TOKEN" ]; then echo "❌ Error: GIT_TOKEN is not set." exit 1 diff --git a/Makefile b/Makefile index c9ef883..b0d0926 100644 --- a/Makefile +++ b/Makefile @@ -30,19 +30,19 @@ build: clean test build-linux build-linux-arm build-darwin build-linux: mkdir -p $(BUILD_DIR)/linux - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION) \ + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION)" \ -o $(BUILD_DIR)/linux/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy cp config.yaml $(BUILD_DIR)/linux build-linux-arm: mkdir -p $(BUILD_DIR)/linux-arm - GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION) \ + GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION)" \ -o $(BUILD_DIR)/linux-arm/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy cp config.yaml $(BUILD_DIR)/linux-arm build-darwin: mkdir -p $(BUILD_DIR)/darwin - GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION) \ + GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION)" \ -o $(BUILD_DIR)/darwin/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy cp config.yaml $(BUILD_DIR)/darwin From 87a1cbe21a5af7dbe2be38b5c748c3f81e474f43 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Sat, 26 Apr 2025 20:02:45 +0530 Subject: [PATCH 35/40] Update release.yml --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e55f6b6..0c51bc7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: with: ref: 'main' fetch-depth: 0 - token: ${{ secrets.GIT_BOT_PAT }} + token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/checkout@v2 - name: Set up Go 1.x @@ -60,5 +60,5 @@ jobs: - name: Update artifact version, package, commit, and create release. env: - GITHUB_TOKEN: ${{ secrets.GIT_BOT_PAT }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bash ./.github/scripts/release.sh $GITHUB_TOKEN ${{ github.workspace }} ${{ github.event.inputs.version_type }} From 4a5cf4e1cc2c5b8ab5ee4cfa0c94080d6493530c Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Sun, 27 Apr 2025 17:23:13 +0530 Subject: [PATCH 36/40] Update README.md --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 71b4b60..6be3ece 100644 --- a/README.md +++ b/README.md @@ -47,15 +47,7 @@ Open MCP Auth Proxy sits between MCP clients and your MCP server to: ### Basic Usage -1. The repository comes with a default `config.yaml` file that contains the basic configuration: - -```yaml -listen_port: 8080 -base_url: "http://localhost:8000" # Your MCP server URL -paths: - sse: "/sse" - messages: "/messages/" -``` +1. Download the latest release from [Github releases](https://github.com/wso2/open-mcp-auth-proxy/releases/latest). 2. Start the proxy in demo mode (uses pre-configured authentication with Asgardeo sandbox): @@ -63,6 +55,16 @@ paths: ./openmcpauthproxy --demo ``` +> The repository comes with a default `config.yaml` file that contains the basic configuration: +> +> ```yaml +> listen_port: 8080 +> base_url: "http://localhost:8000" # Your MCP server URL +> paths: +> sse: "/sse" +> messages: "/messages/" +> ``` + 3. Connect using an MCP client like [MCP Inspector](https://github.com/shashimalcse/inspector)(This is a temporary fork with fixes for authentication [issues](https://github.com/modelcontextprotocol/typescript-sdk/issues/257) in the original implementation) ## Connect an Identity Provider From 0bbc20ca5a1c4f804fb901071109f4a426d8b8de Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Sat, 3 May 2025 01:06:41 +0530 Subject: [PATCH 37/40] Remove unnecessary fields from PR template --- pull_request_template.md | 51 ++++------------------------------------ 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/pull_request_template.md b/pull_request_template.md index 9b32185..c401a06 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,52 +1,11 @@ ## Purpose -> Describe the problems, issues, or needs driving this feature/fix and include links to related issues in the following format: Resolves issue1, issue2, etc. + -## Goals -> Describe the solutions that this feature/fix will introduce to resolve the problems described above - -## Approach -> Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI (email documentation@wso2.com to review all UI text). Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here. - -## User stories -> Summary of user stories addressed by this change> - -## Release note -> Brief description of the new feature or bug fix as it will appear in the release notes - -## Documentation -> Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter “N/A” plus brief explanation of why there’s no doc impact - -## Training -> Link to the PR for changes to the training content in https://github.com/wso2/WSO2-Training, if applicable - -## Certification -> Type “Sent” when you have provided new/updated certification questions, plus four answers for each question (correct answer highlighted in bold), based on this change. Certification questions/answers should be sent to certification@wso2.com and NOT pasted in this PR. If there is no impact on certification exams, type “N/A” and explain why. - -## Marketing -> Link to drafts of marketing content that will describe and promote this feature, including product page changes, technical articles, blog posts, videos, etc., if applicable - -## Automation tests - - Unit tests - > Code coverage information - - Integration tests - > Details about the test cases and coverage - -## Security checks - - Followed secure coding standards in http://wso2.com/technical-reports/wso2-secure-engineering-guidelines? yes/no - - Ran FindSecurityBugs plugin and verified report? yes/no - - Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets? yes/no - -## Samples -> Provide high-level details about the samples related to this feature +## Related Issues + ## Related PRs -> List any other related PRs + ## Migrations (if applicable) -> Describe migration steps and platforms on which migration has been tested - -## Test environment -> List all JDK versions, operating systems, databases, and browser/versions on which this feature/fix was tested - -## Learning -> Describe the research phase and any blog posts, patterns, libraries, or add-ons you used to solve the problem. \ No newline at end of file + From e0beca18cf64b26234348c6ad2b3d71b96ee8703 Mon Sep 17 00:00:00 2001 From: Angel Nunez Mencias Date: Mon, 19 May 2025 10:25:27 +0200 Subject: [PATCH 38/40] adjust configuration to kvant --- .github/workflows/ci.yaml | 71 +++++++++++++++++++++++++++++++++++++++ Dockerfile | 47 ++++++++++++++++++++++++++ cmd/proxy/main.go | 6 ++-- config.yaml | 48 +++++++++++++++++--------- 4 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 Dockerfile diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..775003e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,71 @@ +name: Build and Push container +run-name: Build and Push container +on: + workflow_dispatch: + #schedule: + # - cron: "0 10 * * *" + push: + branches: + - 'main' + - 'master' + tags: + - 'v*' + pull_request: + branches: + - 'main' + - 'master' +env: + IMAGE: git.kvant.cloud/${{github.repository}} +jobs: + build_concierge_backend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set current time + uses: https://github.com/gerred/actions/current-time@master + id: current_time + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to git.kvant.cloud registry + uses: docker/login-action@v3 + with: + registry: git.kvant.cloud + username: ${{ vars.ORG_PACKAGE_WRITER_USERNAME }} + password: ${{ secrets.ORG_PACKAGE_WRITER_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + # list of Docker images to use as base name for tags + images: | + ${{env.IMAGE}} + # generate Docker tags based on the following events/attributes + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + + - name: Build and push to gitea registry + uses: docker/build-push-action@v6 + with: + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + context: . + provenance: mode=max + sbom: true + build-args: | + BUILD_DATE=${{ steps.current_time.outputs.time }} + cache-from: | + type=registry,ref=${{ env.IMAGE }}:buildcache + type=registry,ref=${{ env.IMAGE }}:${{ github.ref_name }} + type=registry,ref=${{ env.IMAGE }}:main + cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max,image-manifest=true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2f0b940 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.24@sha256:d9db32125db0c3a680cfb7a1afcaefb89c898a075ec148fdc2f0f646cc2ed509 AS build + +ARG TARGETPLATFORM +ARG BUILDPLATFORM +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace + +RUN apt update -qq && apt install -qq -y git bash curl g++ + +# Download libraries +ADD go.* . +RUN go mod download + +# Build +ADD cmd cmd +ADD internal internal +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o webhook -ldflags '-w -extldflags "-static"' -o openmcpauthproxy ./cmd/proxy + +#Test +RUN CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go test -v -race ./... + + +# Build production container +FROM --platform=${BUILDPLATFORM:-linux/amd64} ubuntu:24.04 + +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + python3-pip \ + python-is-python3 \ + npm \ + && apt-get autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY --from=build /workspace/openmcpauthproxy /app/ + +ADD config.yaml /app +RUN find . + + +ENTRYPOINT ["/app/openmcpauthproxy"] + +ARG IMAGE_SOURCE +LABEL org.opencontainers.image.source=$IMAGE_SOURCE diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 6424f18..c43dd7d 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -12,7 +12,7 @@ import ( "github.com/wso2/open-mcp-auth-proxy/internal/authz" "github.com/wso2/open-mcp-auth-proxy/internal/config" "github.com/wso2/open-mcp-auth-proxy/internal/constants" - "github.com/wso2/open-mcp-auth-proxy/internal/logging" + logger "github.com/wso2/open-mcp-auth-proxy/internal/logging" "github.com/wso2/open-mcp-auth-proxy/internal/proxy" "github.com/wso2/open-mcp-auth-proxy/internal/subprocess" "github.com/wso2/open-mcp-auth-proxy/internal/util" @@ -58,7 +58,7 @@ func main() { logger.Warn("%v", err) logger.Warn("Subprocess may fail to start due to missing dependencies") } - + procManager = subprocess.NewManager() if err := procManager.Start(cfg); err != nil { logger.Warn("Failed to start subprocess: %v", err) @@ -95,7 +95,7 @@ func main() { // 5. Build the main router mux := proxy.NewRouter(cfg, provider) - listen_address := fmt.Sprintf(":%d", cfg.ListenPort) + listen_address := fmt.Sprintf("0.0.0.0:%d", cfg.ListenPort) // 6. Start the server srv := &http.Server{ diff --git a/config.yaml b/config.yaml index 5621195..af627c9 100644 --- a/config.yaml +++ b/config.yaml @@ -6,13 +6,8 @@ base_url: "http://localhost:8000" # Base URL for the MCP server port: 8000 # Port for the MCP server timeout_seconds: 10 -# Path configuration -paths: - sse: "/sse" # SSE endpoint path - messages: "/messages/" # Messages endpoint path - # Transport mode configuration -transport_mode: "sse" # Options: "sse" or "stdio" +transport_mode: "stdio" # Options: "sse" or "stdio" # stdio-specific configuration (used only when transport_mode is "stdio") stdio: @@ -22,13 +17,10 @@ stdio: # env: # Environment variables (optional) # - "NODE_ENV=development" -# Path mapping (optional) -path_mapping: - -# CORS configuration +# CORS settings cors: allowed_origins: - - "http://localhost:5173" + - "http://localhost:6274" # Origin of your frontend/client app allowed_methods: - "GET" - "POST" @@ -40,8 +32,32 @@ cors: - "mcp-protocol-version" allow_credentials: true -# Demo configuration for Asgardeo -demo: - org_name: "openmcpauthdemo" - client_id: "N0U9e_NNGr9mP_0fPnPfPI0a6twa" - client_secret: "qFHfiBp5gNGAO9zV4YPnDofBzzfInatfUbHyPZvM0jka" +# Keycloak endpoint path mappings +path_mapping: + sse: "/sse" # SSE endpoint path + messages: "/messages/" # Messages endpoint path + /token: /realms/master/protocol/openid-connect/token + /register: /realms/master/clients-registrations/openid-connect + +# Keycloak configuration block +default: + base_url: "https://iam.phoenix-systems.ch" + jwks_url: "https://iam.phoenix-systems.ch/realms/kvant/protocol/openid-connect/certs" + path: + /.well-known/oauth-authorization-server: + response: + issuer: "https://iam.phoenix-systems.ch/realms/kvant" + jwks_uri: "https://iam.phoenix-systems.ch/realms/kvant/protocol/openid-connect/certs" + authorization_endpoint: "https://iam.phoenix-systems.ch/realms/kvant/protocol/openid-connect/auth" + response_types_supported: + - "code" + grant_types_supported: + - "authorization_code" + - "refresh_token" + code_challenge_methods_supported: + - "S256" + - "plain" + /token: + addBodyParams: + - name: "audience" + value: "mcp_proxy" \ No newline at end of file From c3e2abd2bce076d4257d401859b656e6b8f81870 Mon Sep 17 00:00:00 2001 From: Angel Nunez Mencias Date: Mon, 2 Jun 2025 03:41:06 +0200 Subject: [PATCH 39/40] change cmd --- Dockerfile | 1 - config.yaml | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2f0b940..3dc86da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,6 @@ WORKDIR /app COPY --from=build /workspace/openmcpauthproxy /app/ ADD config.yaml /app -RUN find . ENTRYPOINT ["/app/openmcpauthproxy"] diff --git a/config.yaml b/config.yaml index af627c9..ef70fbb 100644 --- a/config.yaml +++ b/config.yaml @@ -6,13 +6,15 @@ base_url: "http://localhost:8000" # Base URL for the MCP server port: 8000 # Port for the MCP server timeout_seconds: 10 + # Transport mode configuration transport_mode: "stdio" # Options: "sse" or "stdio" # stdio-specific configuration (used only when transport_mode is "stdio") stdio: enabled: true - user_command: "npx -y @modelcontextprotocol/server-github" + user_command: uvx mcp-server-time --local-timezone=Europe/Zurich + #user_command: "npx -y @modelcontextprotocol/server-github" work_dir: "" # Working directory (optional) # env: # Environment variables (optional) # - "NODE_ENV=development" From 8e80e2956c273df612ae6114484c6321c2d513be Mon Sep 17 00:00:00 2001 From: Angel Nunez Mencias Date: Mon, 2 Jun 2025 03:55:14 +0200 Subject: [PATCH 40/40] add uvx --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 3dc86da..dc468b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,8 @@ RUN apt-get update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +RUN pip install uvenv --break-system-packages + WORKDIR /app COPY --from=build /workspace/openmcpauthproxy /app/