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 {}
0 commit comments