registry/tools/publisher/auth/github-oidc.go
Radoslav Dimitrov 8d7e471fd3
Allow for vendor extensions on servers (#298)
<!-- Provide a brief summary of your changes -->
This PR serves as a placeholder and brings together all of the changes
related to supporting vendor extensions passed through the API. The idea
is to have a single place to track and review the work as it comes
together (even if it’s still a bit of a work in progress).

**The changes in this PR cover:**
* Updates on the data model
* Updates on both databases (in-memory and postgresql), added a new
table to PSQL for tracking the extensions leaving the server table only
for the clean server.json properties
* Updates the seed.json file (thus the big line change)
* Adds a seed-migrate tool to help migrating an existing seed file to
the new format
* Updates the seed import mechanism to support this
* Updates the existing tests - unit, integration and validation tests
* Adds the publish handler to the OpenAPI spec (it was missing)
* Fixes linting errors
* Updates on the API side on (publish, list, get) to support the new
format of `ServerResponse` -
```
{
  "server": {},
  "x-io.modelcontextprotocol.registry": {}, /*not available for publish since this gets populated by the registry*/
  "x-publisher": {}
}
```

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
Motivated by the discussions in:
https://github.com/modelcontextprotocol/registry/discussions/284

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
Locally and by running all of the tests

## Breaking Changes
<!-- Will users need to update their code or configurations? -->
Yes, it changes both the API and the format of the seed file
 
## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

---

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
Co-authored-by: adam jones <domdomegg+git@gmail.com>
2025-08-23 09:59:03 +01:00

166 lines
4.8 KiB
Go

package auth
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type GitHubOIDCProvider struct {
registryURL string
}
//nolint:ireturn // Factory function returns interface by design
func NewGitHubOIDCProvider(registryURL string) Provider {
return &GitHubOIDCProvider{
registryURL: registryURL,
}
}
// GetToken retrieves the registry JWT token using GitHub Actions OIDC token
func (o *GitHubOIDCProvider) GetToken(ctx context.Context) (string, error) {
// Get OIDC token from GitHub Actions endpoint
oidcToken, err := o.getOIDCTokenFromGitHub(ctx)
if err != nil {
return "", fmt.Errorf("failed to get OIDC token from GitHub: %w", err)
}
// Exchange OIDC token for registry token
registryToken, err := o.exchangeOIDCTokenForRegistry(ctx, oidcToken)
if err != nil {
return "", fmt.Errorf("failed to exchange OIDC token: %w", err)
}
return registryToken, nil
}
// NeedsLogin always returns false for OIDC since the token is provided by GitHub Actions
func (o *GitHubOIDCProvider) NeedsLogin() bool {
// OIDC tokens are provided by GitHub Actions runtime, no interactive login needed
return false
}
// Login is not needed for OIDC since tokens are provided by GitHub Actions
func (o *GitHubOIDCProvider) Login(_ context.Context) error {
// No interactive login needed for OIDC
return nil
}
// Name returns the name of this auth provider
func (o *GitHubOIDCProvider) Name() string {
return "github-oidc"
}
// exchangeOIDCTokenForRegistry exchanges a GitHub OIDC token for a registry JWT token
func (o *GitHubOIDCProvider) exchangeOIDCTokenForRegistry(ctx context.Context, oidcToken string) (string, error) {
if o.registryURL == "" {
return "", fmt.Errorf("registry URL is required for token exchange")
}
// Prepare the request body
payload := map[string]string{
"oidc_token": oidcToken,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// Make the token exchange request
exchangeURL := o.registryURL + "/v0/auth/github-oidc"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, body)
}
var tokenResp RegistryTokenResponse
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return "", fmt.Errorf("failed to unmarshal response: %w", err)
}
return tokenResp.RegistryToken, nil
}
// getOIDCTokenFromGitHub fetches the OIDC token from GitHub Actions endpoint
func (o *GitHubOIDCProvider) getOIDCTokenFromGitHub(ctx context.Context) (string, error) {
// Check for required environment variables
requestToken := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
if requestToken == "" {
return "", fmt.Errorf("ACTIONS_ID_TOKEN_REQUEST_TOKEN environment variable not found - are you running in GitHub Actions with id-token: write permissions?")
}
requestURL := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL")
if requestURL == "" {
return "", fmt.Errorf("ACTIONS_ID_TOKEN_REQUEST_URL environment variable not found - are you running in GitHub Actions with id-token: write permissions?")
}
// Build the full URL with audience parameter
fullURL := requestURL + "&audience=mcp-registry"
// Create the request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// Set the authorization header
req.Header.Set("Authorization", "Bearer "+requestToken)
req.Header.Set("Accept", "application/json")
// Make the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Read the response
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("GitHub OIDC token request failed with status %d: %s", resp.StatusCode, body)
}
// Parse the response to extract the token value
var tokenResp struct {
Value string `json:"value"`
}
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return "", fmt.Errorf("failed to unmarshal OIDC token response: %w", err)
}
if tokenResp.Value == "" {
return "", fmt.Errorf("OIDC token value is empty in response")
}
return tokenResp.Value, nil
}