Skip to content

Commit aafc49f

Browse files
committed
Integrate with credential helpers, use provider name for credential storage keys
1 parent eae2dfe commit aafc49f

File tree

4 files changed

+138
-7
lines changed

4 files changed

+138
-7
lines changed

cmd/docker-mcp/internal/desktop/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func (c *Tools) GetOAuthApp(ctx context.Context, app string) (OAuthApp, error) {
8585

8686
type RegisterDCRRequest struct {
8787
ClientID string `json:"clientId"`
88+
ProviderName string `json:"providerName"`
8889
ClientName string `json:"clientName,omitempty"`
8990
AuthorizationServer string `json:"authorizationServer,omitempty"`
9091
AuthorizationEndpoint string `json:"authorizationEndpoint,omitempty"`
@@ -93,6 +94,7 @@ type RegisterDCRRequest struct {
9394

9495
type DCRClient struct {
9596
ServerName string `json:"serverName"`
97+
ProviderName string `json:"providerName"`
9698
ClientID string `json:"clientId"`
9799
ClientName string `json:"clientName,omitempty"`
98100
RegisteredAt string `json:"registeredAt"` // ISO timestamp

cmd/docker-mcp/internal/mcp/remote.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/modelcontextprotocol/go-sdk/mcp"
1212

1313
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/catalog"
14-
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/desktop"
14+
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/oauth"
1515
)
1616

1717
type remoteMCPClient struct {
@@ -135,16 +135,16 @@ func (c *remoteMCPClient) getOAuthToken(ctx context.Context) (string, error) {
135135
return "", nil
136136
}
137137

138-
// Get the OAuth token from pinata using server name, not provider name
139-
// OAuth tokens are stored by server name (e.g., "notion-remote"), not provider name (e.g., "notion")
140-
client := desktop.NewAuthClient()
141-
app, err := client.GetOAuthApp(ctx, c.config.Name)
142-
if err != nil || !app.Authorized {
138+
// Use secure credential helper to get OAuth token directly from system credential store
139+
// This bypasses the vulnerable IPC endpoint that exposes tokens
140+
credHelper := oauth.NewOAuthCredentialHelper()
141+
token, err := credHelper.GetOAuthToken(ctx, c.config.Name)
142+
if err != nil {
143143
// Token might not exist if user hasn't authorized yet
144144
return "", nil
145145
}
146146

147-
return app.AccessToken, nil
147+
return token, nil
148148
}
149149

150150
// headerRoundTripper is an http.RoundTripper that adds custom headers to all requests
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package oauth
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os/exec"
8+
"strings"
9+
10+
"github.com/docker/docker-credential-helpers/client"
11+
"github.com/docker/docker-credential-helpers/credentials"
12+
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/desktop"
13+
)
14+
15+
// OAuthCredentialHelper provides secure access to OAuth tokens via docker-credential-desktop
16+
type OAuthCredentialHelper struct {
17+
credentialHelper credentials.Helper
18+
}
19+
20+
// NewOAuthCredentialHelper creates a new OAuth credential helper
21+
func NewOAuthCredentialHelper() *OAuthCredentialHelper {
22+
return &OAuthCredentialHelper{
23+
credentialHelper: newOAuthHelper(),
24+
}
25+
}
26+
27+
// GetOAuthToken retrieves an OAuth token for the specified server
28+
// It follows this flow:
29+
// 1. Get DCR client info to retrieve provider name and authorization endpoint
30+
// 2. Construct credential key using: [AuthorizationEndpoint]/[ProviderName]
31+
// 3. Retrieve token from docker-credential-desktop
32+
func (h *OAuthCredentialHelper) GetOAuthToken(ctx context.Context, serverName string) (string, error) {
33+
// Step 1: Get DCR client info (includes stored provider name)
34+
client := desktop.NewAuthClient()
35+
dcrClient, err := client.GetDCRClient(ctx, serverName)
36+
if err != nil {
37+
return "", fmt.Errorf("no DCR client found for %s: %w", serverName, err)
38+
}
39+
40+
// Step 2: Construct credential key using authorization endpoint + provider name
41+
credentialKey := fmt.Sprintf("%s/%s", dcrClient.AuthorizationEndpoint, dcrClient.ProviderName)
42+
43+
// Step 3: Retrieve token from docker-credential-desktop
44+
_, token, err := h.credentialHelper.Get(credentialKey)
45+
if err != nil {
46+
if credentials.IsErrCredentialsNotFound(err) {
47+
return "", fmt.Errorf("OAuth token not found for %s (key: %s). Run 'docker mcp oauth authorize %s' to authenticate", serverName, credentialKey, serverName)
48+
}
49+
return "", fmt.Errorf("failed to retrieve OAuth token for %s: %w", serverName, err)
50+
}
51+
52+
if token == "" {
53+
return "", fmt.Errorf("empty OAuth token found for %s", serverName)
54+
}
55+
56+
return token, nil
57+
}
58+
59+
// newOAuthHelper creates a credential helper for OAuth token access
60+
func newOAuthHelper() credentials.Helper {
61+
return oauthHelper{
62+
program: newShellProgramFunc("docker-credential-desktop"),
63+
}
64+
}
65+
66+
// newShellProgramFunc creates programs that are executed in a Shell.
67+
func newShellProgramFunc(name string) client.ProgramFunc {
68+
return func(args ...string) client.Program {
69+
return &shell{cmd: exec.CommandContext(context.Background(), name, args...)}
70+
}
71+
}
72+
73+
// shell invokes shell commands to talk with a remote credentials-helper.
74+
type shell struct {
75+
cmd *exec.Cmd
76+
}
77+
78+
// Output returns responses from the remote credentials-helper.
79+
func (s *shell) Output() ([]byte, error) {
80+
return s.cmd.Output()
81+
}
82+
83+
// Input sets the input to send to a remote credentials-helper.
84+
func (s *shell) Input(in io.Reader) {
85+
s.cmd.Stdin = in
86+
}
87+
88+
// oauthHelper wraps credential helper program for OAuth token access.
89+
type oauthHelper struct {
90+
program client.ProgramFunc
91+
}
92+
93+
func (h oauthHelper) List() (map[string]string, error) {
94+
return map[string]string{}, nil
95+
}
96+
97+
// Add stores new credentials (not used for OAuth token retrieval)
98+
func (h oauthHelper) Add(creds *credentials.Credentials) error {
99+
return fmt.Errorf("OAuth credential helper is read-only")
100+
}
101+
102+
// Delete removes credentials (not used for OAuth token retrieval)
103+
func (h oauthHelper) Delete(serverURL string) error {
104+
return fmt.Errorf("OAuth credential helper is read-only")
105+
}
106+
107+
// Get returns the OAuth token for a given credential key
108+
func (h oauthHelper) Get(credentialKey string) (string, string, error) {
109+
creds, err := client.Get(h.program, credentialKey)
110+
if err != nil {
111+
return "", "", err
112+
}
113+
return creds.Username, creds.Secret, nil
114+
}
115+
116+
func isErrDecryption(err error) bool {
117+
return err != nil && strings.Contains(err.Error(), "gpg: decryption failed: No secret key")
118+
}
119+
120+
var _ credentials.Helper = oauthHelper{}

cmd/docker-mcp/server/enable.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,18 @@ func setupOAuthForRemoteServer(ctx context.Context, serverName string, cat *cata
158158
return fmt.Errorf("DCR registration failed: %w", err)
159159
}
160160

161+
// Extract provider name from OAuth config
162+
var providerName string
163+
if server.OAuth != nil && len(server.OAuth.Providers) > 0 {
164+
providerName = server.OAuth.Providers[0].Provider // Use first provider
165+
} else {
166+
return fmt.Errorf("no OAuth providers configured for server %s", serverName)
167+
}
168+
161169
// Store DCR client in Docker Desktop
162170
dcrRequest := desktop.RegisterDCRRequest{
163171
ClientID: credentials.ClientID,
172+
ProviderName: providerName,
164173
AuthorizationEndpoint: credentials.AuthorizationEndpoint,
165174
TokenEndpoint: credentials.TokenEndpoint,
166175
}

0 commit comments

Comments
 (0)