mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
feat(auth): introduce jwks url flag to be published in oauth metadata (#197)
This commit is contained in:
@@ -19,12 +19,14 @@ type StaticConfig struct {
|
|||||||
// When true, expose only tools annotated with readOnlyHint=true
|
// When true, expose only tools annotated with readOnlyHint=true
|
||||||
ReadOnly bool `toml:"read_only,omitempty"`
|
ReadOnly bool `toml:"read_only,omitempty"`
|
||||||
// When true, disable tools annotated with destructiveHint=true
|
// When true, disable tools annotated with destructiveHint=true
|
||||||
DisableDestructive bool `toml:"disable_destructive,omitempty"`
|
DisableDestructive bool `toml:"disable_destructive,omitempty"`
|
||||||
EnabledTools []string `toml:"enabled_tools,omitempty"`
|
EnabledTools []string `toml:"enabled_tools,omitempty"`
|
||||||
DisabledTools []string `toml:"disabled_tools,omitempty"`
|
DisabledTools []string `toml:"disabled_tools,omitempty"`
|
||||||
RequireOAuth bool `toml:"require_oauth,omitempty"`
|
RequireOAuth bool `toml:"require_oauth,omitempty"`
|
||||||
AuthorizationURL string `toml:"authorization_url,omitempty"`
|
AuthorizationURL string `toml:"authorization_url,omitempty"`
|
||||||
ServerURL string `toml:"server_url,omitempty"`
|
JwksURL string `toml:"jwks_url,omitempty"`
|
||||||
|
CertificateAuthority string `toml:"certificate_authority,omitempty"`
|
||||||
|
ServerURL string `toml:"server_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroupVersionKind struct {
|
type GroupVersionKind struct {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.Stat
|
|||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"authorization_servers": authServers,
|
"authorization_servers": authServers,
|
||||||
|
"authorization_server": authServers[0],
|
||||||
"scopes_supported": []string{},
|
"scopes_supported": []string{},
|
||||||
"bearer_methods_supported": []string{"header"},
|
"bearer_methods_supported": []string{"header"},
|
||||||
}
|
}
|
||||||
@@ -68,6 +69,10 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.Stat
|
|||||||
response["resource"] = staticConfig.ServerURL
|
response["resource"] = staticConfig.ServerURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if staticConfig.JwksURL != "" {
|
||||||
|
response["jwks_uri"] = staticConfig.JwksURL
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -46,20 +50,22 @@ kubernetes-mcp-server --port 8443 --sse-base-url https://example.com:8443
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MCPServerOptions struct {
|
type MCPServerOptions struct {
|
||||||
Version bool
|
Version bool
|
||||||
LogLevel int
|
LogLevel int
|
||||||
Port string
|
Port string
|
||||||
SSEPort int
|
SSEPort int
|
||||||
HttpPort int
|
HttpPort int
|
||||||
SSEBaseUrl string
|
SSEBaseUrl string
|
||||||
Kubeconfig string
|
Kubeconfig string
|
||||||
Profile string
|
Profile string
|
||||||
ListOutput string
|
ListOutput string
|
||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
DisableDestructive bool
|
DisableDestructive bool
|
||||||
RequireOAuth bool
|
RequireOAuth bool
|
||||||
AuthorizationURL string
|
AuthorizationURL string
|
||||||
ServerURL string
|
JwksURL string
|
||||||
|
CertificateAuthority string
|
||||||
|
ServerURL string
|
||||||
|
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
StaticConfig *config.StaticConfig
|
StaticConfig *config.StaticConfig
|
||||||
@@ -116,8 +122,13 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
|
|||||||
_ = cmd.Flags().MarkHidden("require-oauth")
|
_ = cmd.Flags().MarkHidden("require-oauth")
|
||||||
cmd.Flags().StringVar(&o.AuthorizationURL, "authorization-url", o.AuthorizationURL, "OAuth authorization server URL for protected resource endpoint. If not provided, the Kubernetes API server host will be used. Only valid if require-oauth is enabled.")
|
cmd.Flags().StringVar(&o.AuthorizationURL, "authorization-url", o.AuthorizationURL, "OAuth authorization server URL for protected resource endpoint. If not provided, the Kubernetes API server host will be used. Only valid if require-oauth is enabled.")
|
||||||
_ = cmd.Flags().MarkHidden("authorization-url")
|
_ = cmd.Flags().MarkHidden("authorization-url")
|
||||||
|
cmd.Flags().StringVar(&o.JwksURL, "jwks-url", o.JwksURL, "OAuth JWKS server URL for protected resource endpoint. Only valid if require-oauth is enabled.")
|
||||||
|
_ = cmd.Flags().MarkHidden("jwks-url")
|
||||||
cmd.Flags().StringVar(&o.ServerURL, "server-url", o.ServerURL, "Server URL of this application. Optional. If set, this url will be served in protected resource metadata endpoint and tokens will be validated with this audience. If not set, expected audience is kubernetes-mcp-server. Only valid if require-oauth is enabled.")
|
cmd.Flags().StringVar(&o.ServerURL, "server-url", o.ServerURL, "Server URL of this application. Optional. If set, this url will be served in protected resource metadata endpoint and tokens will be validated with this audience. If not set, expected audience is kubernetes-mcp-server. Only valid if require-oauth is enabled.")
|
||||||
_ = cmd.Flags().MarkHidden("server-url")
|
_ = cmd.Flags().MarkHidden("server-url")
|
||||||
|
cmd.Flags().StringVar(&o.CertificateAuthority, "certificate-authority", o.CertificateAuthority, "Certificate authority path to verify certificates. Optional. Only valid if require-oauth is enabled.")
|
||||||
|
_ = cmd.Flags().MarkHidden("certificate-authority")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,9 +185,15 @@ func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) {
|
|||||||
if cmd.Flag("authorization-url").Changed {
|
if cmd.Flag("authorization-url").Changed {
|
||||||
m.StaticConfig.AuthorizationURL = m.AuthorizationURL
|
m.StaticConfig.AuthorizationURL = m.AuthorizationURL
|
||||||
}
|
}
|
||||||
|
if cmd.Flag("jwks-url").Changed {
|
||||||
|
m.StaticConfig.JwksURL = m.JwksURL
|
||||||
|
}
|
||||||
if cmd.Flag("server-url").Changed {
|
if cmd.Flag("server-url").Changed {
|
||||||
m.StaticConfig.ServerURL = m.ServerURL
|
m.StaticConfig.ServerURL = m.ServerURL
|
||||||
}
|
}
|
||||||
|
if cmd.Flag("certificate-authority").Changed {
|
||||||
|
m.StaticConfig.CertificateAuthority = m.CertificateAuthority
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MCPServerOptions) initializeLogging() {
|
func (m *MCPServerOptions) initializeLogging() {
|
||||||
@@ -195,8 +212,8 @@ func (m *MCPServerOptions) Validate() error {
|
|||||||
if m.Port != "" && (m.SSEPort > 0 || m.HttpPort > 0) {
|
if m.Port != "" && (m.SSEPort > 0 || m.HttpPort > 0) {
|
||||||
return fmt.Errorf("--port is mutually exclusive with deprecated --http-port and --sse-port flags")
|
return fmt.Errorf("--port is mutually exclusive with deprecated --http-port and --sse-port flags")
|
||||||
}
|
}
|
||||||
if !m.StaticConfig.RequireOAuth && (m.StaticConfig.AuthorizationURL != "" || m.StaticConfig.ServerURL != "") {
|
if !m.StaticConfig.RequireOAuth && (m.StaticConfig.AuthorizationURL != "" || m.StaticConfig.ServerURL != "" || m.StaticConfig.JwksURL != "" || m.StaticConfig.CertificateAuthority != "") {
|
||||||
return fmt.Errorf("authorization-url and server-url are only valid if require-oauth is enabled")
|
return fmt.Errorf("authorization-url, server-url, certificate-authority and jwks-url are only valid if require-oauth is enabled. Missing --port may implicitly set require-oauth to false")
|
||||||
}
|
}
|
||||||
if m.StaticConfig.AuthorizationURL != "" {
|
if m.StaticConfig.AuthorizationURL != "" {
|
||||||
u, err := url.Parse(m.StaticConfig.AuthorizationURL)
|
u, err := url.Parse(m.StaticConfig.AuthorizationURL)
|
||||||
@@ -222,6 +239,18 @@ func (m *MCPServerOptions) Validate() error {
|
|||||||
klog.Warningf("server-url is using http://, this is not recommended production use")
|
klog.Warningf("server-url is using http://, this is not recommended production use")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if m.StaticConfig.JwksURL != "" {
|
||||||
|
u, err := url.Parse(m.StaticConfig.JwksURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.Scheme != "https" && u.Scheme != "http" {
|
||||||
|
return fmt.Errorf("--jwks-url must be a valid URL")
|
||||||
|
}
|
||||||
|
if u.Scheme == "http" {
|
||||||
|
klog.Warningf("jwks-url is using http://, this is not recommended production use")
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +277,31 @@ func (m *MCPServerOptions) Run() error {
|
|||||||
|
|
||||||
var oidcProvider *oidc.Provider
|
var oidcProvider *oidc.Provider
|
||||||
if m.StaticConfig.AuthorizationURL != "" {
|
if m.StaticConfig.AuthorizationURL != "" {
|
||||||
provider, err := oidc.NewProvider(context.TODO(), m.StaticConfig.AuthorizationURL)
|
ctx := context.Background()
|
||||||
|
if m.StaticConfig.CertificateAuthority != "" {
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
caCert, err := os.ReadFile(m.StaticConfig.CertificateAuthority)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read CA certificate from %s: %w", m.StaticConfig.CertificateAuthority, err)
|
||||||
|
}
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||||
|
return fmt.Errorf("failed to append CA certificate from %s to pool", m.StaticConfig.CertificateAuthority)
|
||||||
|
}
|
||||||
|
|
||||||
|
if caCertPool.Equal(x509.NewCertPool()) {
|
||||||
|
caCertPool = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
httpClient.Transport = transport
|
||||||
|
ctx = oidc.ClientContext(ctx, httpClient)
|
||||||
|
}
|
||||||
|
provider, err := oidc.NewProvider(ctx, m.StaticConfig.AuthorizationURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to setup OIDC provider: %w", err)
|
return fmt.Errorf("unable to setup OIDC provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user