feat(publisher): integrate GitHub Client ID into device flow authentication

This commit is contained in:
Avinash Sridhar 2025-05-25 16:48:29 -04:00 • committed by Toby Padilla
commit 4dbf80562b
2 changed files with 48 additions and 36 deletions

View file

@ -20,8 +20,7 @@ type Authentication struct {
// PublishRequest represents a request to publish a server to the registry // PublishRequest represents a request to publish a server to the registry
type PublishRequest struct { type PublishRequest struct {
ServerDetail `json:",inline"` ServerDetail `json:",inline"`
Authentication Authentication `json:"-"` // Now provided via Authorization header AuthStatusToken string `json:"-"` // Used internally for device flows
AuthStatusToken string `json:"-"` // Used internally for device flows
} }
// Repository represents a source code repository as defined in the spec // Repository represents a source code repository as defined in the spec
@ -121,7 +120,7 @@ type Server struct {
// ServerDetail represents detailed server information as defined in the spec // ServerDetail represents detailed server information as defined in the spec
type ServerDetail struct { type ServerDetail struct {
Server `json:",inline" bson:",inline"` Server `json:",inline" bson:",inline"`
Packages []Package `json:"packages,omitempty" bson:"packages,omitempty"` Packages []Package `json:"packages,omitempty" bson:"packages,omitempty"`
Remotes []Remote `json:"remotes,omitempty" bson:"remotes,omitempty"` Remotes []Remote `json:"remotes,omitempty" bson:"remotes,omitempty"`
} }

View file

@ -18,9 +18,6 @@ const (
// GitHub OAuth URLs // GitHub OAuth URLs
GitHubDeviceCodeURL = "https://github.com/login/device/code" GitHubDeviceCodeURL = "https://github.com/login/device/code"
GitHubAccessTokenURL = "https://github.com/login/oauth/access_token" GitHubAccessTokenURL = "https://github.com/login/oauth/access_token"
// Environment variable for GitHub Client ID
EnvGithubClientID = "MCP_REGISTRY_GITHUB_CLIENT_ID"
) )
// DeviceCodeResponse represents the response from GitHub's device code endpoint // DeviceCodeResponse represents the response from GitHub's device code endpoint
@ -40,6 +37,11 @@ type AccessTokenResponse struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
type ServerHealthResponse struct {
Status string `json:"status"`
GitHubClientId string `json:"github_client_id"`
}
func main() { func main() {
var registryURL string var registryURL string
var mcpFilePath string var mcpFilePath string
@ -58,18 +60,31 @@ func main() {
return return
} }
// Check for GitHub client ID in environment if we're going to need it for authentication // get the clientID from the server's health endpoint
if providedToken == "" && os.Getenv(EnvGithubClientID) == "" { healthURL := registryURL + "/v0/health"
fmt.Printf("Warning: Environment variable %s is not set. This is required for GitHub authentication.\n", EnvGithubClientID) resp, err := http.Get(healthURL)
fmt.Println("You can set it with: export " + EnvGithubClientID + "=your_github_client_id") if err != nil {
fmt.Println("Or provide a token directly with the --token flag.") fmt.Printf("Error fetching health endpoint: %s\n", err.Error())
return
// Only return if we'll need to do GitHub auth
_, statErr := os.Stat(tokenFilePath)
if forceLogin || os.IsNotExist(statErr) {
return
}
} }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Health endpoint returned status %d: %s\n", resp.StatusCode, body)
return
}
var healthResponse ServerHealthResponse
err = json.NewDecoder(resp.Body).Decode(&healthResponse)
if err != nil {
fmt.Printf("Error decoding health response: %s\n", err.Error())
return
}
if healthResponse.GitHubClientId == "" {
fmt.Println("GitHub Client ID is not set in the server's health response.")
return
}
githubClientID := healthResponse.GitHubClientId
var token string var token string
@ -80,7 +95,7 @@ func main() {
// Check if token exists or force login is requested // Check if token exists or force login is requested
_, statErr := os.Stat(tokenFilePath) _, statErr := os.Stat(tokenFilePath)
if forceLogin || os.IsNotExist(statErr) { if forceLogin || os.IsNotExist(statErr) {
err := performDeviceFlowLogin() err := performDeviceFlowLogin(githubClientID)
if err != nil { if err != nil {
fmt.Printf("Failed to perform device flow login: %s\n", err.Error()) fmt.Printf("Failed to perform device flow login: %s\n", err.Error())
return return
@ -113,15 +128,15 @@ func main() {
fmt.Println("Successfully published to registry!") fmt.Println("Successfully published to registry!")
} }
func performDeviceFlowLogin() error { func performDeviceFlowLogin(githubClientID string) error {
// Check if the environment variable is set
if os.Getenv(EnvGithubClientID) == "" { if githubClientID == "" {
return fmt.Errorf("environment variable %s must be set for GitHub authentication", EnvGithubClientID) return fmt.Errorf("GitHub Client ID is required for device flow login")
} }
// Device flow login logic using GitHub's device flow // Device flow login logic using GitHub's device flow
// First, request a device code // First, request a device code
deviceCode, userCode, verificationURI, err := requestDeviceCode() deviceCode, userCode, verificationURI, err := requestDeviceCode(githubClientID)
if err != nil { if err != nil {
return fmt.Errorf("error requesting device code: %w", err) return fmt.Errorf("error requesting device code: %w", err)
} }
@ -134,7 +149,7 @@ func performDeviceFlowLogin() error {
// Poll for the token // Poll for the token
fmt.Println("Waiting for authorization...") fmt.Println("Waiting for authorization...")
token, err := pollForToken(deviceCode) token, err := pollForToken(deviceCode, githubClientID)
if err != nil { if err != nil {
return fmt.Errorf("error polling for token: %w", err) return fmt.Errorf("error polling for token: %w", err)
} }
@ -150,14 +165,13 @@ func performDeviceFlowLogin() error {
} }
// requestDeviceCode initiates the device authorization flow // requestDeviceCode initiates the device authorization flow
func requestDeviceCode() (string, string, string, error) { func requestDeviceCode(githubClientID string) (string, string, string, error) {
clientID := os.Getenv(EnvGithubClientID) if githubClientID == "" {
if clientID == "" { return "", "", "", fmt.Errorf("GitHub Client ID is required for device flow login")
return "", "", "", fmt.Errorf("environment variable %s is not set", EnvGithubClientID)
} }
payload := map[string]string{ payload := map[string]string{
"client_id": clientID, "client_id": githubClientID,
"scope": "read:org read:user", "scope": "read:org read:user",
} }
@ -199,14 +213,13 @@ func requestDeviceCode() (string, string, string, error) {
} }
// pollForToken polls for access token after user completes authorization // pollForToken polls for access token after user completes authorization
func pollForToken(deviceCode string) (string, error) { func pollForToken(deviceCode, githubClientID string) (string, error) {
clientID := os.Getenv(EnvGithubClientID) if githubClientID == "" {
if clientID == "" { return "", fmt.Errorf("GitHub Client ID is required for device flow login")
return "", fmt.Errorf("environment variable %s is not set", EnvGithubClientID)
} }
payload := map[string]string{ payload := map[string]string{
"client_id": clientID, "client_id": githubClientID,
"device_code": deviceCode, "device_code": deviceCode,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
} }