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
53 changes: 37 additions & 16 deletions cmd/docker-mcp/oauth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,36 @@ import (
"fmt"
"time"

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

func Authorize(ctx context.Context, app string, scopes string) error {
// Check if running in CE mode
if pkgoauth.IsCEMode() {
// Check if running in CE mode AND if this is a remote server
// CE mode with localhost redirect only works for remote MCP servers
// Container-based servers (like github-official) need Docker Desktop OAuth
if pkgoauth.IsCEMode() && isRemoteServer(ctx, app) {
return authorizeCEMode(ctx, app, scopes)
}

// Desktop mode - existing implementation
// Desktop mode - existing implementation (for container servers or when CE mode is off)
return authorizeDesktopMode(ctx, app, scopes)
}

// isRemoteServer checks if the server is a remote MCP server (not a container)
func isRemoteServer(ctx context.Context, serverName string) bool {
cat, err := catalog.GetWithOptions(ctx, true, nil)
if err != nil {
return false
}
server, found := cat.Servers[serverName]
if !found {
return false
}
return server.Remote.URL != ""
}

// authorizeDesktopMode handles OAuth via Docker Desktop (existing behavior)
func authorizeDesktopMode(ctx context.Context, app string, scopes string) error {
client := desktop.NewAuthClient()
Expand All @@ -42,22 +58,28 @@ func authorizeDesktopMode(ctx context.Context, app string, scopes string) error
func authorizeCEMode(ctx context.Context, serverName string, scopes string) error {
fmt.Printf("Starting OAuth authorization for %s...\n", serverName)

// Create OAuth manager with read-write credential helper
// Step 1: Create callback server FIRST to get the localhost URL
// This allows us to register DCR with localhost redirect instead of mcp.docker.com proxy
callbackServer, err := pkgoauth.NewCallbackServer()
if err != nil {
return fmt.Errorf("failed to create callback server: %w", err)
}

// Use localhost callback URL as redirect URI for DCR registration
// This bypasses Docker's OAuth proxy which doesn't know about all providers
callbackURL := callbackServer.URL()
fmt.Printf("Using localhost redirect: %s\n", callbackURL)

// Create OAuth manager with localhost redirect URI
credHelper := pkgoauth.NewReadWriteCredentialHelper()
manager := pkgoauth.NewManager(credHelper)
manager := pkgoauth.NewManagerWithRedirectURI(credHelper, callbackURL)

// Step 1: Ensure DCR client is registered
// Step 2: Ensure DCR client is registered with localhost redirect
fmt.Printf("Checking DCR registration...\n")
if err := manager.EnsureDCRClient(ctx, serverName, scopes); err != nil {
return fmt.Errorf("DCR registration failed: %w", err)
}

// Step 2: Create callback server
callbackServer, err := pkgoauth.NewCallbackServer()
if err != nil {
return fmt.Errorf("failed to create callback server: %w", err)
}

// Start callback server in background
go func() {
if err := callbackServer.Start(); err != nil {
Expand All @@ -72,17 +94,16 @@ func authorizeCEMode(ctx context.Context, serverName string, scopes string) erro
}
}()

// Step 3: Build authorization URL with callback URL in state
// Step 3: Build authorization URL (no proxy state needed - direct localhost redirect)
fmt.Printf("Generating authorization URL...\n")

scopesList := []string{}
if scopes != "" {
scopesList = []string{scopes}
}

// Pass callback URL - will be embedded in state for mcp-oauth proxy routing
callbackURL := callbackServer.URL()
authURL, baseState, _, err := manager.BuildAuthorizationURL(ctx, serverName, scopesList, callbackURL)
// Empty callbackURL since we're using direct localhost redirect (no proxy routing)
authURL, baseState, _, err := manager.BuildAuthorizationURL(ctx, serverName, scopesList, "")
if err != nil {
return fmt.Errorf("failed to generate authorization URL: %w", err)
}
Expand Down
10 changes: 8 additions & 2 deletions pkg/oauth/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ type Manager struct {

// NewManager creates a new OAuth manager for CE mode
func NewManager(credHelper credentials.Helper) *Manager {
return NewManagerWithRedirectURI(credHelper, DefaultRedirectURI)
}

// NewManagerWithRedirectURI creates a new OAuth manager with a custom redirect URI
// Use this for CE mode with localhost callbacks
func NewManagerWithRedirectURI(credHelper credentials.Helper, redirectURI string) *Manager {
return &Manager{
dcrManager: dcr.NewManager(credHelper, DefaultRedirectURI),
dcrManager: dcr.NewManager(credHelper, redirectURI),
tokenStore: NewTokenStore(credHelper),
stateManager: NewStateManager(),
redirectURI: DefaultRedirectURI,
redirectURI: redirectURI,
}
}

Expand Down
12 changes: 12 additions & 0 deletions pkg/oauth/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,18 @@ func TestManager_SetRedirectURI(t *testing.T) {
assert.Equal(t, customURI, manager.redirectURI)
}

func TestNewManagerWithRedirectURI(t *testing.T) {
helper := newFakeCredentialHelper()
localhostURI := "http://localhost:5001/callback"

manager := NewManagerWithRedirectURI(helper, localhostURI)

assert.Equal(t, localhostURI, manager.redirectURI)
assert.NotNil(t, manager.dcrManager)
assert.NotNil(t, manager.tokenStore)
assert.NotNil(t, manager.stateManager)
}

func TestManager_EnsureDCRClient_AlreadyExists(t *testing.T) {
manager := setupTestManager(t)
serverName := "test-server"
Expand Down