Skip to content

Commit 2089c5a

Browse files
sridharavinashtoby
authored andcommitted
feat(auth): Implement GitHub OAuth authentication flow for package publishing
- Added authentication service and GitHub OAuth integration. - Introduced new authentication methods and structures in the model. - Updated the API to handle authentication during the publishing process. - Created handlers for starting and checking the status of the OAuth flow. - Enhanced the publish handler to validate authentication credentials. - Updated configuration to include GitHub OAuth settings. - Added tests for the OAuth flow and publishing with authentication. - Updated documentation to reflect changes in the API and authentication process.
1 parent 582d93a commit 2089c5a

File tree

18 files changed

+924
-35
lines changed

18 files changed

+924
-35
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
build/*
22
data
3+
.env

.vscode/launch.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
8+
{
9+
"name": "Launch server",
10+
"type": "go",
11+
"request": "launch",
12+
"mode": "auto",
13+
"program": "${workspaceFolder}/cmd/registry",
14+
"envFile": "${workspaceFolder}/.env",
15+
},
16+
{
17+
"name": "Launch publisher",
18+
"type": "go",
19+
"request": "launch",
20+
"mode": "auto",
21+
"program": "${workspaceFolder}/tools/publisher/main.go",
22+
"args": [
23+
"-registry-url=http://localhost:8080",
24+
"-mcp-file=${workspaceFolder}/tools/publisher/mcp.json",
25+
],
26+
}
27+
]
28+
}

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ lint:
7979
build-all: clean
8080
@echo "Building for multiple platforms..."
8181
@mkdir -p $(BUILD_DIR)
82-
@GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PATH)
83-
@GOOS=darwin GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PATH)
84-
@GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PATH)
85-
@GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PATH)
82+
@GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE)
83+
@GOOS=darwin GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE)
84+
@GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PACKAGE)
85+
@GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE)
8686
@echo "Cross-compilation complete!"
8787

8888
# Show help

cmd/registry/main.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/modelcontextprotocol/registry/internal/api"
15+
"github.com/modelcontextprotocol/registry/internal/auth"
1516
"github.com/modelcontextprotocol/registry/internal/config"
1617
"github.com/modelcontextprotocol/registry/internal/database"
1718
"github.com/modelcontextprotocol/registry/internal/service"
@@ -70,8 +71,23 @@ func main() {
7071
}()
7172
}
7273

74+
// Initialize authentication services
75+
authService := auth.NewAuthService(cfg)
76+
77+
if cfg.RequireAuth {
78+
log.Println("Authentication is required for package publishing")
79+
80+
if cfg.GithubClientID == "" || cfg.GithubClientSecret == "" {
81+
log.Println("Warning: GitHub OAuth credentials not configured but authentication is required")
82+
} else {
83+
log.Println("GitHub OAuth authentication is configured")
84+
}
85+
} else {
86+
log.Println("Authentication is optional for package publishing")
87+
}
88+
7389
// Initialize HTTP server
74-
server := api.NewServer(cfg, registryService)
90+
server := api.NewServer(cfg, registryService, authService)
7591

7692
// Start server in a goroutine so it doesn't block signal handling
7793
go func() {

internal/api/handlers/v0/auth.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Package v0 contains API handlers for version 0 of the API
2+
package v0
3+
4+
import (
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
9+
"github.com/modelcontextprotocol/registry/internal/auth"
10+
"github.com/modelcontextprotocol/registry/internal/model"
11+
)
12+
13+
// StartAuthHandler handles requests to start an authentication flow
14+
func StartAuthHandler(authService auth.Service) http.HandlerFunc {
15+
return func(w http.ResponseWriter, r *http.Request) {
16+
// Only allow POST method
17+
if r.Method != http.MethodPost {
18+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
19+
return
20+
}
21+
22+
// Read the request body
23+
body, err := io.ReadAll(r.Body)
24+
if err != nil {
25+
http.Error(w, "Error reading request body", http.StatusBadRequest)
26+
return
27+
}
28+
defer r.Body.Close()
29+
30+
// Parse request body into AuthRequest struct
31+
var authReq struct {
32+
Method string `json:"method"`
33+
RepoRef string `json:"repo_ref"`
34+
}
35+
err = json.Unmarshal(body, &authReq)
36+
if err != nil {
37+
http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest)
38+
return
39+
}
40+
41+
// Validate required fields
42+
if authReq.Method == "" {
43+
http.Error(w, "Auth method is required", http.StatusBadRequest)
44+
return
45+
}
46+
47+
// Convert string method to enum type
48+
var method model.AuthMethod
49+
switch authReq.Method {
50+
case "github":
51+
method = model.AuthMethodGitHub
52+
default:
53+
http.Error(w, "Unsupported authentication method", http.StatusBadRequest)
54+
return
55+
}
56+
57+
// Start auth flow
58+
flowInfo, statusToken, err := authService.StartAuthFlow(r.Context(), method, authReq.RepoRef)
59+
if err != nil {
60+
http.Error(w, "Failed to start auth flow: "+err.Error(), http.StatusInternalServerError)
61+
return
62+
}
63+
64+
// Return successful response
65+
w.Header().Set("Content-Type", "application/json")
66+
w.WriteHeader(http.StatusOK)
67+
json.NewEncoder(w).Encode(map[string]interface{}{
68+
"flow_info": flowInfo,
69+
"status_token": statusToken,
70+
"expires_in": 300, // 5 minutes
71+
})
72+
}
73+
}
74+
75+
// CheckAuthStatusHandler handles requests to check the status of an authentication flow
76+
func CheckAuthStatusHandler(authService auth.Service) http.HandlerFunc {
77+
return func(w http.ResponseWriter, r *http.Request) {
78+
// Only allow GET method
79+
if r.Method != http.MethodGet {
80+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
81+
return
82+
}
83+
84+
// Get status token from query parameter
85+
statusToken := r.URL.Query().Get("token")
86+
if statusToken == "" {
87+
http.Error(w, "Status token is required", http.StatusBadRequest)
88+
return
89+
}
90+
91+
// Check auth status
92+
token, err := authService.CheckAuthStatus(r.Context(), statusToken)
93+
if err != nil {
94+
if err.Error() == "pending" {
95+
// Auth is still pending
96+
w.Header().Set("Content-Type", "application/json")
97+
w.WriteHeader(http.StatusOK)
98+
json.NewEncoder(w).Encode(map[string]interface{}{
99+
"status": "pending",
100+
})
101+
return
102+
}
103+
104+
// Other error
105+
http.Error(w, "Failed to check auth status: "+err.Error(), http.StatusInternalServerError)
106+
return
107+
}
108+
109+
// Authentication completed successfully
110+
w.Header().Set("Content-Type", "application/json")
111+
w.WriteHeader(http.StatusOK)
112+
json.NewEncoder(w).Encode(map[string]interface{}{
113+
"status": "complete",
114+
"token": token,
115+
})
116+
}
117+
}

internal/api/handlers/v0/publish.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,24 @@ import (
66
"io"
77
"net/http"
88

9+
"github.com/modelcontextprotocol/registry/internal/auth"
910
"github.com/modelcontextprotocol/registry/internal/model"
1011
"github.com/modelcontextprotocol/registry/internal/service"
1112
)
1213

14+
// httpError represents an HTTP error with a message and status code
15+
type httpError struct {
16+
msg string
17+
status int
18+
}
19+
20+
// Error returns the error message
21+
func (e *httpError) Error() string {
22+
return e.msg
23+
}
24+
1325
// PublishHandler handles requests to publish new server details to the registry
14-
func PublishHandler(registry service.RegistryService) http.HandlerFunc {
26+
func PublishHandler(registry service.RegistryService, authService auth.Service) http.HandlerFunc {
1527
return func(w http.ResponseWriter, r *http.Request) {
1628
// Only allow POST method
1729
if r.Method != http.MethodPost {
@@ -27,26 +39,48 @@ func PublishHandler(registry service.RegistryService) http.HandlerFunc {
2739
}
2840
defer r.Body.Close()
2941

30-
// Parse request body into ServerDetail struct
31-
var serverDetail model.ServerDetail
32-
err = json.Unmarshal(body, &serverDetail)
42+
// Parse request body into PublishRequest struct
43+
var publishReq model.PublishRequest
44+
err = json.Unmarshal(body, &publishReq)
3345
if err != nil {
34-
http.Error(w, "Invalid request payload", http.StatusBadRequest)
46+
http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest)
3547
return
3648
}
3749

50+
// Get server details from the request
51+
serverDetail := publishReq.ServerDetail
52+
3853
// Validate required fields
3954
if serverDetail.Name == "" {
4055
http.Error(w, "Name is required", http.StatusBadRequest)
4156
return
4257
}
4358

44-
// version is required
59+
// Version is required
4560
if serverDetail.VersionDetail.Version == "" {
4661
http.Error(w, "Version is required", http.StatusBadRequest)
4762
return
4863
}
4964

65+
// Validate authentication credentials
66+
if authService != nil {
67+
publishReq.Authentication.RepoRef = serverDetail.Name
68+
valid, err := authService.ValidateAuth(r.Context(), publishReq.Authentication)
69+
if err != nil {
70+
if err == auth.ErrAuthRequired {
71+
http.Error(w, "Authentication is required for publishing", http.StatusUnauthorized)
72+
return
73+
}
74+
http.Error(w, "Authentication failed: "+err.Error(), http.StatusUnauthorized)
75+
return
76+
}
77+
78+
if !valid {
79+
http.Error(w, "Invalid authentication credentials", http.StatusUnauthorized)
80+
return
81+
}
82+
}
83+
5084
// Call the publish method on the registry service
5185
err = registry.Publish(&serverDetail)
5286
if err != nil {

internal/api/router/router.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ package router
44
import (
55
"net/http"
66

7+
"github.com/modelcontextprotocol/registry/internal/auth"
78
"github.com/modelcontextprotocol/registry/internal/config"
89
"github.com/modelcontextprotocol/registry/internal/service"
910
)
1011

1112
// New creates a new router with all API versions registered
12-
func New(cfg *config.Config, registry service.RegistryService) *http.ServeMux {
13+
func New(cfg *config.Config, registry service.RegistryService, authService auth.Service) *http.ServeMux {
1314
mux := http.NewServeMux()
1415

1516
// Register routes for all API versions
16-
RegisterV0Routes(mux, cfg, registry)
17+
RegisterV0Routes(mux, cfg, registry, authService)
1718

1819
return mux
1920
}

internal/api/router/v0.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@ import (
55
"net/http"
66

77
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
8+
"github.com/modelcontextprotocol/registry/internal/auth"
89
"github.com/modelcontextprotocol/registry/internal/config"
910
"github.com/modelcontextprotocol/registry/internal/service"
1011
)
1112

1213
// RegisterV0Routes registers all v0 API routes to the provided router
13-
func RegisterV0Routes(mux *http.ServeMux, cfg *config.Config, registry service.RegistryService) {
14+
func RegisterV0Routes(mux *http.ServeMux, cfg *config.Config, registry service.RegistryService, authService auth.Service) {
1415
// Register v0 endpoints
1516
mux.HandleFunc("/v0/health", v0.HealthHandler())
1617
mux.HandleFunc("/v0/servers", v0.ServersHandler(registry))
1718
mux.HandleFunc("/v0/servers/{id}", v0.ServersDetailHandler(registry))
1819
mux.HandleFunc("/v0/ping", v0.PingHandler(cfg))
19-
mux.HandleFunc("/v0/publish", v0.PublishHandler(registry))
20+
mux.HandleFunc("/v0/publish", v0.PublishHandler(registry, authService))
2021

2122
// Register Swagger UI routes
2223
mux.HandleFunc("/v0/swagger/", v0.SwaggerHandler())

internal/api/server.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@ import (
66
"net/http"
77

88
"github.com/modelcontextprotocol/registry/internal/api/router"
9+
"github.com/modelcontextprotocol/registry/internal/auth"
910
"github.com/modelcontextprotocol/registry/internal/config"
1011
"github.com/modelcontextprotocol/registry/internal/service"
1112
)
1213

1314
// Server represents the HTTP server
1415
type Server struct {
15-
config *config.Config
16-
registry service.RegistryService
17-
router *http.ServeMux
18-
server *http.Server
16+
config *config.Config
17+
registry service.RegistryService
18+
authService auth.Service
19+
router *http.ServeMux
20+
server *http.Server
1921
}
2022

2123
// NewServer creates a new HTTP server
22-
func NewServer(cfg *config.Config, registryService service.RegistryService) *Server {
24+
func NewServer(cfg *config.Config, registryService service.RegistryService, authService auth.Service) *Server {
2325
// Create router with all API versions registered
24-
mux := router.New(cfg, registryService)
26+
mux := router.New(cfg, registryService, authService)
2527

2628
server := &Server{
27-
config: cfg,
28-
registry: registryService,
29-
router: mux,
29+
config: cfg,
30+
registry: registryService,
31+
authService: authService,
32+
router: mux,
3033
server: &http.Server{
3134
Addr: cfg.ServerAddress,
3235
Handler: mux,

internal/auth/auth.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Package auth provides authentication mechanisms for the MCP registry
2+
package auth
3+
4+
import (
5+
"context"
6+
"errors"
7+
8+
"github.com/modelcontextprotocol/registry/internal/model"
9+
)
10+
11+
var (
12+
// ErrAuthRequired is returned when authentication is required but not provided
13+
ErrAuthRequired = errors.New("authentication required")
14+
// ErrUnsupportedAuthMethod is returned when an unsupported auth method is used
15+
ErrUnsupportedAuthMethod = errors.New("unsupported authentication method")
16+
)
17+
18+
// Service defines the authentication service interface
19+
type Service interface {
20+
// StartAuthFlow initiates an authentication flow and returns the flow information
21+
StartAuthFlow(ctx context.Context, method model.AuthMethod, repoRef string) (map[string]string, string, error)
22+
23+
// CheckAuthStatus checks the status of an authentication flow using a status token
24+
CheckAuthStatus(ctx context.Context, statusToken string) (string, error)
25+
26+
// ValidateAuth validates the authentication credentials
27+
ValidateAuth(ctx context.Context, auth model.Authentication) (bool, error)
28+
}

0 commit comments

Comments
 (0)