<!-- 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>
252 lines
7.8 KiB
Go
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
|
|
}
|