Skip to content

feat: poc for oauth support in mcp server - keycloak#138

Draft
Hardikl wants to merge 3 commits into
mainfrom
hl_oauth
Draft

feat: poc for oauth support in mcp server - keycloak#138
Hardikl wants to merge 3 commits into
mainfrom
hl_oauth

Conversation

@Hardikl
Copy link
Copy Markdown
Contributor

@Hardikl Hardikl commented May 15, 2026

No description provided.

Copilot AI review requested due to automatic review settings May 15, 2026 15:11
@cla-bot cla-bot Bot added the cla-signed label May 15, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an initial (proof-of-concept) OAuth 2.0 / OIDC bearer-token authentication layer to the MCP HTTP server. When configured, the server validates JWTs against a JWKS endpoint, exposes /.well-known/oauth-protected-resource metadata per RFC 9728, and challenges unauthenticated callers with a WWW-Authenticate header pointing at that metadata URL. Configuration is taken from CLI flags, environment variables, or an optional .ontap-mcp.env file.

Changes:

  • New server/oauth.go with a JWKS-backed JWT middleware (audience/issuer/exp/scope checks) plus a request-logging middleware.
  • server/server.go reworks runHTTPServer to use a real http.ServeMux, conditionally mounting the OAuth middleware + protected-resource metadata endpoint, and adds a loadEnv helper that reads .ontap-mcp.env.
  • New CLI/Options fields (oauth-server-url, jwks-url, resource-url) and the corresponding dependencies (MicahParks/keyfunc/v3, golang-jwt/jwt/v5).

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
server/oauth.go New OAuth/JWT validation middleware, helpers, and request-logging middleware.
server/server.go Wires OAuth config + protected-resource metadata into the HTTP server; adds .ontap-mcp.env loader and new Options fields.
cmd/cmds.go Adds CLI/env flags for OAuth server URL, JWKS URL, and resource URL, and passes them into server options.
go.mod Adds MicahParks/keyfunc/v3 and indirect deps (MicahParks/jwkset, golang-jwt/jwt/v5, golang.org/x/time).
go.sum Checksum updates for the newly added modules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/server.go
Comment on lines +794 to +814
}
defer func() {
if err := file.Close(); err != nil {
slog.Warn("failed to close file", slog.Any("error", err))
}
}()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if os.Getenv(key) == "" {
if err = os.Setenv(key, value); err != nil {
// Log the error and proceed further
slog.Error("Error setting environment variable", slog.String("key", key), slog.String("value", value), slog.Any("err", err))
Comment thread server/oauth.go
func (c *OAuthConfig) sendUnauthorized(w http.ResponseWriter, _ *http.Request) {
metadataURL := c.ResourceURL + "/.well-known/oauth-protected-resource"
w.Header().Set("WWW-Authenticate",
fmt.Sprintf(`Bearer resource_metadata="%q", scope="openid profile email"`, metadataURL))
Comment thread server/server.go
Comment on lines +294 to +305
mux.HandleFunc("/.well-known/oauth-protected-resource", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
return
}
metadata := oauthex.ProtectedResourceMetadata{
Resource: oauthConfig.ResourceURL,
ScopesSupported: []string{"mcp:tools"},
AuthorizationServers: []string{oauthConfig.AuthServerURL},
}

handler.ServeHTTP(w, r)
})
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(metadata); err != nil {
a.logger.Error("metadata encoding failed", slog.Any("error", err))
}
Comment thread server/oauth.go
Comment on lines +144 to +149
exp, ok := claims["exp"].(float64)
if !ok {
return false
}
// Allow 60 seconds of clock skew
return time.Now().Unix() < int64(exp)+60
Comment thread server/server.go
Comment on lines +293 to +329
if oAthExist {
mux.HandleFunc("/.well-known/oauth-protected-resource", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
return
}
metadata := oauthex.ProtectedResourceMetadata{
Resource: oauthConfig.ResourceURL,
ScopesSupported: []string{"mcp:tools"},
AuthorizationServers: []string{oauthConfig.AuthServerURL},
}

handler.ServeHTTP(w, r)
})
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(metadata); err != nil {
a.logger.Error("metadata encoding failed", slog.Any("error", err))
}
})

// MCP endpoint (OAuth authorization required, with logging)
mux.Handle("/", LoggingMiddleware(oauthConfig.OAuthMiddleware(handler)))
} else {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Skip MCP handler for health endpoint
if r.URL.Path == "/health" {
http.DefaultServeMux.ServeHTTP(w, r)
return
}

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Mcp-Protocol-Version, Mcp-Session-Id")

if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}

handler.ServeHTTP(w, r)
})
}
Comment thread server/oauth.go
}

func (c *OAuthConfig) sendUnauthorized(w http.ResponseWriter, _ *http.Request) {
metadataURL := c.ResourceURL + "/.well-known/oauth-protected-resource"
Comment thread server/server.go
Comment on lines +247 to +249
a.logger.Info("MCP Server started with Oauth")
} else {
a.logger.Info("MCP Server started without any Oauth")
Comment thread server/server.go
Port int
ReadOnly bool
Stateless bool
OauthServerURL string
Comment thread server/oauth.go
// Validate expiration
// Note: jwt.Parse already validates exp by default, but we explicitly check here for clarity
if !c.validateExpiration(claims) {
slog.Error("token has been expired")
Comment thread server/server.go
Comment on lines +792 to +793
// .ontap-mcp.env file doesn't exist, that's okay
slog.Warn(".ontap-mcp.env file not exist", slog.Any("error", err))
# Conflicts:
#	cmd/cmds.go
#	go.mod
#	go.sum
#	server/server.go
@Hardikl Hardikl marked this pull request as draft May 15, 2026 15:16
@Hardikl Hardikl linked an issue May 15, 2026 that may be closed by this pull request
@Hardikl Hardikl linked an issue May 15, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ontap-mcp should support OAuth Authentication

2 participants