registry/cmd/publisher/commands/login.go
Radoslav Dimitrov f5f08bc92d
Implement --token and MCP_GITHUB_TOKEN login support (#705)
<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
The following PR implements the missing support for logging in via
`--token` or `MCP_GITHUB_TOKEN`

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

## 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)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->
Fixes: #698

---------

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
2025-10-29 17:23:41 +02:00

252 lines
7.8 KiB
Go

package commands
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/modelcontextprotocol/registry/cmd/publisher/auth"
"github.com/modelcontextprotocol/registry/cmd/publisher/auth/azurekeyvault"
"github.com/modelcontextprotocol/registry/cmd/publisher/auth/googlekms"
)
const (
DefaultRegistryURL = "https://registry.modelcontextprotocol.io"
TokenFileName = ".mcp_publisher_token" //nolint:gosec // Not a credential, just a filename
MethodGitHub = "github"
MethodGitHubOIDC = "github-oidc"
MethodDNS = "dns"
MethodHTTP = "http"
MethodNone = "none"
)
type CryptoAlgorithm auth.CryptoAlgorithm
type SignerType string
type LoginFlags struct {
Domain string
PrivateKey string
RegistryURL string
KvVault string
KvKeyName string
KmsResource string
Token Token
CryptoAlgorithm CryptoAlgorithm
SignerType SignerType
ArgOffset int
}
const (
InProcessSignerType SignerType = "in-process"
AzureKeyVaultSignerType SignerType = "azure-key-vault"
GoogleKMSSignerType SignerType = "google-kms"
NoSignerType SignerType = "none"
)
func (c *CryptoAlgorithm) String() string {
return string(*c)
}
func (c *CryptoAlgorithm) Set(v string) error {
switch v {
case string(auth.AlgorithmEd25519), string(auth.AlgorithmECDSAP384):
*c = CryptoAlgorithm(v)
return nil
}
return fmt.Errorf("invalid algorithm: %q (allowed: ed25519, ecdsap384)", v)
}
type Token string
func parseLoginFlags(method string, args []string) (LoginFlags, error) {
var flags LoginFlags
loginFlags := flag.NewFlagSet("login", flag.ExitOnError)
flags.CryptoAlgorithm = CryptoAlgorithm(auth.AlgorithmEd25519)
flags.SignerType = NoSignerType
flags.ArgOffset = 1
loginFlags.StringVar(&flags.RegistryURL, "registry", DefaultRegistryURL, "Registry URL")
// Add --token flag for GitHub authentication
var token string
if method == MethodGitHub {
loginFlags.StringVar(&token, "token", "", "GitHub Personal Access Token")
}
if method == "dns" || method == "http" {
loginFlags.StringVar(&flags.Domain, "domain", "", "Domain name")
if len(args) > 1 {
switch args[1] {
case string(AzureKeyVaultSignerType):
flags.SignerType = AzureKeyVaultSignerType
loginFlags.StringVar(&flags.KvVault, "vault", "", "The name of the Azure Key Vault resource")
loginFlags.StringVar(&flags.KvKeyName, "key", "", "Name of the signing key in the Azure Key Vault")
flags.ArgOffset = 2
case string(GoogleKMSSignerType):
flags.SignerType = GoogleKMSSignerType
loginFlags.StringVar(&flags.KmsResource, "resource", "", "Google Cloud KMS resource name (e.g. projects/lotr/locations/global/keyRings/fellowship/cryptoKeys/frodo/cryptoKeyVersions/1)")
flags.ArgOffset = 2
}
}
if flags.SignerType == NoSignerType {
flags.SignerType = InProcessSignerType
loginFlags.StringVar(&flags.PrivateKey, "private-key", "", "Private key (hex)")
loginFlags.Var(&flags.CryptoAlgorithm, "algorithm", "Cryptographic algorithm (ed25519, ecdsap384)")
}
}
err := loginFlags.Parse(args[flags.ArgOffset:])
if err == nil {
flags.RegistryURL = strings.TrimRight(flags.RegistryURL, "/")
}
// Store the token in flags if it was provided
if method == MethodGitHub {
flags.Token = Token(token)
}
return flags, err
}
func createSigner(flags LoginFlags) (auth.Signer, error) {
switch flags.SignerType {
case AzureKeyVaultSignerType:
return azurekeyvault.GetSignatureProvider(flags.KvVault, flags.KvKeyName)
case GoogleKMSSignerType:
return googlekms.GetSignatureProvider(flags.KmsResource)
case InProcessSignerType:
return auth.NewInProcessSigner(flags.PrivateKey, auth.CryptoAlgorithm(flags.CryptoAlgorithm))
case NoSignerType:
return nil, errors.New("no signing provider specified")
default:
return nil, errors.New("unknown signing provider specified")
}
}
func createAuthProvider(method, registryURL, domain string, token Token, signer auth.Signer) (auth.Provider, error) {
switch method {
case MethodGitHub:
return auth.NewGitHubATProvider(true, registryURL, string(token)), nil
case MethodGitHubOIDC:
return auth.NewGitHubOIDCProvider(registryURL), nil
case MethodDNS:
if domain == "" {
return nil, errors.New("dns authentication requires --domain")
}
return auth.NewDNSProvider(registryURL, domain, &signer), nil
case MethodHTTP:
if domain == "" {
return nil, errors.New("http authentication requires --domain")
}
return auth.NewHTTPProvider(registryURL, domain, &signer), nil
case MethodNone:
return auth.NewNoneProvider(registryURL), nil
default:
return nil, fmt.Errorf("unknown authentication method: %s\nFor a list of available methods, run: mcp-publisher login", method)
}
}
func LoginCommand(args []string) error {
if len(args) < 1 {
return errors.New(`authentication method required
Usage: mcp-publisher login <method> [<signing provider>]
Methods:
github Interactive GitHub authentication
github-oidc GitHub Actions OIDC authentication
dns DNS-based authentication (requires --domain)
http HTTP-based authentication (requires --domain)
none Anonymous authentication (for testing)
Signing providers:
azure-key-vault Sign using Azure Key Vault
google-kms Sign using Google Cloud KMS
The dns and http methods require a --private-key for in-process signing. For
out-of-process signing, use one of the supported signing providers. Signing is
needed for an authentication challenge with the registry.
The github and github-oidc methods do not support signing providers and
authenticate using the GitHub as an identity provider.
Examples:
# Interactive GitHub login, using device code flow
mcp-publisher login github
# Sign in using a specific Ed25519 private key for DNS authentication
mcp-publisher login dns -algorithm ed25519 -domain example.com -private-key <64 hex chars>
# Sign in using a specific ECDSA P-384 private key for DNS authentication
mcp-publisher login dns -algorithm ecdsap384 -domain example.com -private-key <96 hex chars>
# Sign in with gcloud CLI, use Google Cloud KMS for signing in DNS authentication
gcloud auth application-default login
mcp-publisher login dns google-kms -domain example.com -resource projects/lotr/locations/global/keyRings/fellowship/cryptoKeys/frodo/cryptoKeyVersions/1
# Sign in with az CLI, use Azure Key Vault for signing in HTTP authentication
az login
mcp-publisher login http azure-key-vault -domain example.com -vault myvault -key mysigningkey
`)
}
method := args[0]
flags, err := parseLoginFlags(method, args)
if err != nil {
return err
}
var signer auth.Signer
if flags.SignerType != NoSignerType {
signer, err = createSigner(flags)
if err != nil {
return err
}
}
authProvider, err := createAuthProvider(method, flags.RegistryURL, flags.Domain, flags.Token, signer)
if err != nil {
return err
}
ctx := context.Background()
_, _ = fmt.Fprintf(os.Stdout, "Logging in with %s...\n", method)
if err := authProvider.Login(ctx); err != nil {
return fmt.Errorf("login failed: %w", err)
}
token, err := authProvider.GetToken(ctx)
if err != nil {
return fmt.Errorf("failed to get token: %w", err)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
tokenPath := filepath.Join(homeDir, TokenFileName)
tokenData := map[string]string{
"token": token,
"method": method,
"registry": flags.RegistryURL,
}
jsonData, err := json.Marshal(tokenData)
if err != nil {
return fmt.Errorf("failed to marshal token data: %w", err)
}
if err := os.WriteFile(tokenPath, jsonData, 0600); err != nil {
return fmt.Errorf("failed to save token: %w", err)
}
_, _ = fmt.Fprintln(os.Stdout, "✓ Successfully logged in")
return nil
}