Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions cmd/docker-mcp/commands/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func oauthCommand() *cobra.Command {
cmd.AddCommand(lsOauthCommand())
cmd.AddCommand(authorizeOauthCommand())
cmd.AddCommand(revokeOauthCommand())
cmd.AddCommand(registerOauthCommand())
return cmd
}

Expand Down Expand Up @@ -61,3 +62,48 @@ func revokeOauthCommand() *cobra.Command {
},
}
}

func registerOauthCommand() *cobra.Command {
var opts oauth.RegisterOptions
cmd := &cobra.Command{
Use: "register <server-name>",
Short: "Manually register OAuth client credentials for a server.",
Long: `Manually register OAuth client credentials for servers that don't support Dynamic Client Registration (DCR).

This command allows you to configure pre-registered OAuth client credentials from your OAuth provider.
After registration, you can authorize with: docker mcp oauth authorize <server-name>

Examples:
# Register with client ID and secret (confidential client)
docker mcp oauth register my-server \
--client-id "abc123" \
--client-secret "secret456" \
--auth-endpoint "https://provider.com/oauth/authorize" \
--token-endpoint "https://provider.com/oauth/token" \
--scopes "read,write"

# Register public client (no secret)
docker mcp oauth register my-server \
--client-id "public-client-id" \
--auth-endpoint "https://provider.com/oauth/authorize" \
--token-endpoint "https://provider.com/oauth/token"`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return oauth.Register(cmd.Context(), args[0], opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.ClientID, "client-id", "", "OAuth client ID (required)")
flags.StringVar(&opts.ClientSecret, "client-secret", "", "OAuth client secret (optional, for confidential clients)")
flags.StringVar(&opts.AuthorizationEndpoint, "auth-endpoint", "", "Authorization endpoint URL (required)")
flags.StringVar(&opts.TokenEndpoint, "token-endpoint", "", "Token endpoint URL (required)")
flags.StringVar(&opts.Scopes, "scopes", "", "Comma-separated list of OAuth scopes")
flags.StringVar(&opts.Provider, "provider", "", "Provider name (defaults to server name)")
flags.StringVar(&opts.ResourceURL, "resource-url", "", "Resource URL for the OAuth provider (defaults to auth endpoint base)")

_ = cmd.MarkFlagRequired("client-id")
_ = cmd.MarkFlagRequired("auth-endpoint")
_ = cmd.MarkFlagRequired("token-endpoint")

return cmd
}
24 changes: 24 additions & 0 deletions cmd/docker-mcp/oauth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import (
"fmt"
"time"

"github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/secret"
"github.com/docker/mcp-gateway/pkg/desktop"
"github.com/docker/mcp-gateway/pkg/log"
pkgoauth "github.com/docker/mcp-gateway/pkg/oauth"
)

// clientSecretSuffix is the naming convention for OAuth client secrets in the secrets store.
const clientSecretSuffix = ".client_secret"

func Authorize(ctx context.Context, app string, scopes string) error {
// Check if running in CE mode
if pkgoauth.IsCEMode() {
Expand All @@ -23,6 +28,25 @@ func Authorize(ctx context.Context, app string, scopes string) error {
func authorizeDesktopMode(ctx context.Context, app string, scopes string) error {
client := desktop.NewAuthClient()

// For pre-registered OAuth clients, re-register the DCR client with the latest
// client_secret from the Secrets Engine. The user may have set the secret after
// the server was added to the profile.
if dcrClient, err := client.GetDCRClient(ctx, app); err == nil && dcrClient.ClientID != "" {
clientSecretKey := secret.GetDefaultSecretKey(app + clientSecretSuffix)
if env, err := secret.GetSecret(ctx, clientSecretKey); err == nil && string(env.Value) != "" {
req := desktop.RegisterDCRRequest{
ProviderName: dcrClient.ProviderName,
ClientID: dcrClient.ClientID,
ClientSecret: string(env.Value),
AuthorizationEndpoint: dcrClient.AuthorizationEndpoint,
TokenEndpoint: dcrClient.TokenEndpoint,
}
if err := client.RegisterDCRClientPending(ctx, app, req); err != nil {
log.Logf("Warning: failed to update DCR client with client_secret: %v", err)
}
}
}

// Start OAuth flow - Docker Desktop handles DCR automatically if needed
authResponse, err := client.PostOAuthApp(ctx, app, scopes, false)
if err != nil {
Expand Down
132 changes: 132 additions & 0 deletions cmd/docker-mcp/oauth/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package oauth

import (
"context"
"fmt"
"net/url"
"strings"
"time"

pkgoauth "github.com/docker/mcp-gateway/pkg/oauth"
"github.com/docker/mcp-gateway/pkg/oauth/dcr"
)

// RegisterOptions contains configuration for manually registering OAuth credentials
type RegisterOptions struct {
ClientID string
ClientSecret string
AuthorizationEndpoint string
TokenEndpoint string
Scopes string
Provider string
ResourceURL string
}

// Register manually registers OAuth client credentials for a server
// This is used when the OAuth provider does not support Dynamic Client Registration (DCR)
func Register(_ context.Context, serverName string, opts RegisterOptions) error {
// Validate required fields
if err := validateRegisterOptions(serverName, opts); err != nil {
return err
}

// Parse scopes
var scopesList []string
if opts.Scopes != "" {
scopesList = strings.Split(opts.Scopes, ",")
// Trim whitespace from each scope
for i, scope := range scopesList {
scopesList[i] = strings.TrimSpace(scope)
}
}

// Use server name as provider if not specified
provider := opts.Provider
if provider == "" {
provider = serverName
}

// Use authorization endpoint as resource URL if not specified
resourceURL := opts.ResourceURL
if resourceURL == "" {
// Try to extract base URL from authorization endpoint
if u, err := url.Parse(opts.AuthorizationEndpoint); err == nil {
resourceURL = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}
}

// Create DCR client struct
client := dcr.Client{
ServerName: serverName,
ClientID: opts.ClientID,
ClientSecret: opts.ClientSecret,
ProviderName: provider,
AuthorizationEndpoint: opts.AuthorizationEndpoint,
TokenEndpoint: opts.TokenEndpoint,
RequiredScopes: scopesList,
ResourceURL: resourceURL,
RegisteredAt: time.Now(),
ClientName: fmt.Sprintf("MCP Gateway - %s (manual)", serverName),
}

// Store in credential helper
credHelper := pkgoauth.NewReadWriteCredentialHelper()
credentials := dcr.NewCredentials(credHelper)

if err := credentials.SaveClient(serverName, client); err != nil {
return fmt.Errorf("failed to store OAuth credentials: %w", err)
}

fmt.Printf("Successfully registered OAuth client for server: %s\n", serverName)
fmt.Printf(" Provider: %s\n", provider)
fmt.Printf(" Client ID: %s\n", opts.ClientID)
if opts.ClientSecret != "" {
fmt.Printf(" Client Secret: [configured]\n")
} else {
fmt.Printf(" Client Secret: [none - public client]\n")
}
fmt.Printf(" Authorization Endpoint: %s\n", opts.AuthorizationEndpoint)
fmt.Printf(" Token Endpoint: %s\n", opts.TokenEndpoint)
if len(scopesList) > 0 {
fmt.Printf(" Scopes: %s\n", strings.Join(scopesList, ", "))
}
fmt.Printf("\nYou can now authorize with: docker mcp oauth authorize %s\n", serverName)

return nil
}

// validateRegisterOptions validates the registration options
func validateRegisterOptions(serverName string, opts RegisterOptions) error {
if serverName == "" {
return fmt.Errorf("server name is required")
}

if opts.ClientID == "" {
return fmt.Errorf("client-id is required")
}

if opts.AuthorizationEndpoint == "" {
return fmt.Errorf("auth-endpoint is required")
}

if opts.TokenEndpoint == "" {
return fmt.Errorf("token-endpoint is required")
}

// Validate URLs
if _, err := url.Parse(opts.AuthorizationEndpoint); err != nil {
return fmt.Errorf("invalid auth-endpoint URL: %w", err)
}

if _, err := url.Parse(opts.TokenEndpoint); err != nil {
return fmt.Errorf("invalid token-endpoint URL: %w", err)
}

if opts.ResourceURL != "" {
if _, err := url.Parse(opts.ResourceURL); err != nil {
return fmt.Errorf("invalid resource-url: %w", err)
}
}

return nil
}
6 changes: 3 additions & 3 deletions cmd/docker-mcp/server/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ func update(ctx context.Context, docker docker.Client, dockerCli command.Cli, ad
Ref: "",
}

// DCR flag enabled AND type="remote" AND oauth present
if mcpOAuthDcrEnabled && server.HasExplicitOAuthProviders() {
// DCR flag enabled AND (remote OAuth server OR pre-registered OAuth from catalog)
if mcpOAuthDcrEnabled && (server.HasExplicitOAuthProviders() || server.HasPreRegisteredOAuth()) {
// In CE mode, skip lazy setup - DCR happens during oauth authorize
if pkgoauth.IsCEMode() {
fmt.Printf("OAuth server %s enabled. Run 'docker mcp oauth authorize %s' to authenticate\n", serverName, serverName)
Expand All @@ -74,7 +74,7 @@ func update(ctx context.Context, docker docker.Client, dockerCli command.Cli, ad
fmt.Printf("OAuth provider configured for %s - use 'docker mcp oauth authorize %s' to authenticate\n", serverName, serverName)
}
}
} else if !mcpOAuthDcrEnabled && server.HasExplicitOAuthProviders() {
} else if !mcpOAuthDcrEnabled && (server.HasExplicitOAuthProviders() || server.HasPreRegisteredOAuth()) {
// Provide guidance when DCR is needed but disabled
fmt.Printf("Server %s requires OAuth authentication but DCR is disabled.\n", serverName)
fmt.Printf(" To enable automatic OAuth setup, run: docker mcp feature enable mcp-oauth-dcr\n")
Expand Down
Loading
Loading