Skip to content

Commit 1608de9

Browse files
committed
init
1 parent f2d497a commit 1608de9

File tree

11 files changed

+952
-0
lines changed

11 files changed

+952
-0
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Global owners
2+
* @docker/ai-tools-team

.golangci.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
version: "2"
2+
linters:
3+
default: none
4+
enable:
5+
- containedctx
6+
- copyloopvar
7+
- errcheck
8+
- ginkgolinter
9+
- gocritic
10+
- govet
11+
- importas # Enforces consistent import aliases.
12+
- ineffassign
13+
- misspell
14+
- nakedret
15+
- nolintlint
16+
- revive
17+
- staticcheck
18+
- unconvert
19+
- unparam
20+
- unused
21+
- usestdlibvars
22+
- noctx
23+
- nilnil
24+
- testifylint
25+
- intrange
26+
- thelper
27+
- forbidigo
28+
29+
settings:
30+
forbidigo:
31+
forbid:
32+
- pattern: ^os\.UserHomeDir
33+
message: "Do not use os.UserHomeDir(). Use user.HomeDir() instead."
34+
gocritic:
35+
disabled-checks:
36+
- ifElseChain
37+
- exitAfterDefer
38+
exclusions:
39+
generated: lax
40+
presets:
41+
- comments
42+
- std-error-handling
43+
formatters:
44+
enable:
45+
- gofmt
46+
- gofumpt
47+
- goimports
48+
settings:
49+
gofmt:
50+
simplify: false
51+
rewrite-rules:
52+
- pattern: interface{}
53+
replacement: any
54+
goimports:
55+
local-prefixes:
56+
- github.com/docker/mcp-gateway-oauth-helpers

Dockerfile

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#syntax=docker/dockerfile:1
2+
3+
ARG GO_VERSION=1.24.5
4+
5+
FROM --platform=${BUILDPLATFORM} golangci/golangci-lint:v2.1.6-alpine AS lint-base
6+
7+
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine AS base
8+
RUN apk add --no-cache git rsync
9+
WORKDIR /app
10+
11+
FROM base AS lint
12+
COPY --from=lint-base /usr/bin/golangci-lint /usr/bin/golangci-lint
13+
ARG TARGETOS
14+
ARG TARGETARCH
15+
RUN --mount=target=. \
16+
--mount=type=cache,target=/go/pkg/mod \
17+
--mount=type=cache,target=/root/.cache/go-build \
18+
--mount=type=cache,target=/root/.cache/golangci-lint <<EOD
19+
set -e
20+
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} golangci-lint --timeout 30m0s run ./...
21+
EOD
22+
23+
FROM base AS test
24+
ARG TARGETOS
25+
ARG TARGETARCH
26+
RUN --mount=target=. \
27+
--mount=type=cache,target=/go/pkg/mod \
28+
--mount=type=cache,target=/root/.cache/go-build <<EOD
29+
set -e
30+
CGO_ENABLED=0 go test -short --count=1 -v ./...
31+
EOD
32+
33+
FROM base AS do-format
34+
RUN --mount=type=cache,target=/go/pkg/mod \
35+
--mount=type=cache,target=/root/.cache/go-build \
36+
go install golang.org/x/tools/cmd/goimports@latest \
37+
&& go install mvdan.cc/gofumpt@latest
38+
COPY . .
39+
RUN goimports -local github.com/docker/mcp-gateway-oauth-helpers -w .
40+
RUN gofumpt -w .
41+
42+
FROM scratch AS format
43+
COPY --from=do-format /app .

Makefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Makefile for mcp-gateway-oauth-helpers library
2+
3+
.PHONY: format lint test clean
4+
5+
# Format Go code using Docker
6+
format:
7+
docker buildx build --target=format -o . .
8+
9+
# Run linting using Docker
10+
lint:
11+
docker buildx build --target=lint --platform=linux,darwin,windows .
12+
13+
# Run linting for specific platform
14+
lint-%:
15+
docker buildx build --target=lint --platform=$* .
16+
17+
# Run tests using Docker
18+
test:
19+
docker buildx build --target=test .
20+
21+
# Clean build cache
22+
clean:
23+
docker buildx prune -f

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# MCP Gateway OAuth Helpers
2+
3+
Library containing OAuth Dynamic Client Registration (DCR) functionality for MCP servers.
4+
5+
## Purpose
6+
7+
This library provides the core OAuth/DCR functions for MCP Gateway:
8+
9+
- **OAuth Discovery**: Discover OAuth requirements from MCP servers (RFC 9728 + 8414)
10+
- **Dynamic Client Registration**: Register OAuth clients automatically (RFC 7591)
11+
- **WWW-Authenticate Parsing**: Parse OAuth challenge headers

SECURITY.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Security Policy
2+
3+
The maintainers of Docker MCP Gateway take security seriously. If you discover
4+
a security issue, please bring it to their attention right away!
5+
6+
## Reporting a Vulnerability
7+
8+
Please **DO NOT** file a public issue, instead send your report privately
9+
10+
11+
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
12+
received.
13+
14+
## Review Process
15+
16+
After receiving the report, an initial triage and technical analysis is
17+
performed to confirm the report and determine its scope. We may request
18+
additional information in this stage of the process.
19+
20+
Once a reviewer has confirmed the relevance of the report, a draft security
21+
advisory will be created on GitHub. The draft advisory will be used to discuss
22+
the issue with maintainers, the reporter(s), and where applicable, other
23+
affected parties under embargo.
24+
25+
If the vulnerability is accepted, a timeline for developing a patch, public
26+
disclosure, and patch release will be determined. If there is an embargo period
27+
on public disclosure before the patch release, the reporter(s) are expected to
28+
participate in the discussion of the timeline and abide by agreed upon dates
29+
for public disclosure.
30+
31+
## Accreditation
32+
33+
Security reports are greatly appreciated and we will publicly thank you,
34+
although we will keep your name confidential if you request it. We also like to
35+
send gifts - if you're into swag, make sure to let us know. We do not currently
36+
offer a paid security bounty program at this time.

dcr.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package oauth
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
)
11+
12+
// PerformDCR performs Dynamic Client Registration with the authorization server
13+
// Returns client credentials for the registered public client
14+
//
15+
// RFC 7591 COMPLIANCE:
16+
// - Uses token_endpoint_auth_method="none" for public clients
17+
// - Includes redirect_uris pointing to mcp-oauth proxy
18+
// - Requests authorization_code and refresh_token grant types
19+
func PerformDCR(ctx context.Context, discovery *Discovery, serverName string) (*ClientCredentials, error) {
20+
if discovery.RegistrationEndpoint == "" {
21+
return nil, fmt.Errorf("no registration endpoint found for %s", serverName)
22+
}
23+
24+
// Build DCR request for PUBLIC client
25+
registration := DCRRequest{
26+
ClientName: fmt.Sprintf("MCP Gateway - %s", serverName),
27+
RedirectURIs: []string{
28+
"https://mcp.docker.com/oauth/callback", // mcp-oauth proxy callback only
29+
},
30+
TokenEndpointAuthMethod: "none", // PUBLIC client (no client secret)
31+
GrantTypes: []string{"authorization_code", "refresh_token"},
32+
ResponseTypes: []string{"code"},
33+
34+
// Additional metadata for better client identification
35+
ClientURI: "https://github.com/docker/mcp-gateway",
36+
SoftwareID: "mcp-gateway",
37+
SoftwareVersion: "1.0.0",
38+
Contacts: []string{"[email protected]"},
39+
}
40+
41+
// Add requested scopes if provided
42+
if len(discovery.Scopes) > 0 {
43+
registration.Scope = joinScopes(discovery.Scopes)
44+
} else {
45+
}
46+
47+
// Marshal the registration request
48+
body, err := json.Marshal(registration)
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to marshal DCR request: %w", err)
51+
}
52+
53+
// Create HTTP request
54+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, discovery.RegistrationEndpoint, bytes.NewReader(body))
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to create DCR request: %w", err)
57+
}
58+
59+
req.Header.Set("Content-Type", "application/json")
60+
req.Header.Set("Accept", "application/json")
61+
req.Header.Set("User-Agent", "MCP-Gateway/1.0.0")
62+
63+
// Send the request
64+
client := &http.Client{}
65+
resp, err := client.Do(req)
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to send DCR request to %s: %w", discovery.RegistrationEndpoint, err)
68+
}
69+
defer resp.Body.Close()
70+
71+
// Check response status (201 Created or 200 OK are acceptable)
72+
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
73+
// Read error response body to understand why DCR failed
74+
errorBody, err := io.ReadAll(resp.Body)
75+
if err != nil {
76+
return nil, fmt.Errorf("DCR failed with status %d for %s", resp.StatusCode, serverName)
77+
}
78+
79+
errorMsg := string(errorBody)
80+
81+
// Try to parse as JSON for structured error
82+
var errorResp map[string]any
83+
if err := json.Unmarshal(errorBody, &errorResp); err == nil {
84+
// Successfully parsed as JSON - look for common error fields
85+
if errDesc, ok := errorResp["error_description"].(string); ok {
86+
errorMsg = errDesc
87+
} else if errField, ok := errorResp["error"].(string); ok {
88+
errorMsg = errField
89+
} else if message, ok := errorResp["message"].(string); ok {
90+
errorMsg = message
91+
}
92+
}
93+
94+
return nil, fmt.Errorf("DCR failed with status %d for %s: %s", resp.StatusCode, serverName, errorMsg)
95+
}
96+
97+
// Parse the response
98+
var dcrResponse DCRResponse
99+
if err := json.NewDecoder(resp.Body).Decode(&dcrResponse); err != nil {
100+
return nil, fmt.Errorf("failed to decode DCR response: %w", err)
101+
}
102+
103+
if dcrResponse.ClientID == "" {
104+
return nil, fmt.Errorf("DCR response missing client_id for %s", serverName)
105+
}
106+
107+
// Create client credentials (public client - no secret)
108+
creds := &ClientCredentials{
109+
ClientID: dcrResponse.ClientID,
110+
ServerURL: discovery.ResourceURL,
111+
IsPublic: true,
112+
AuthorizationEndpoint: discovery.AuthorizationEndpoint,
113+
TokenEndpoint: discovery.TokenEndpoint,
114+
// No ClientSecret for public clients
115+
}
116+
117+
return creds, nil
118+
}
119+
120+
// joinScopes joins a slice of scopes into a space-separated string
121+
// per OAuth 2.0 specification (RFC 6749 Section 3.3)
122+
func joinScopes(scopes []string) string {
123+
if len(scopes) == 0 {
124+
return ""
125+
}
126+
127+
// Use simple string concatenation for small arrays
128+
result := scopes[0]
129+
for i := 1; i < len(scopes); i++ {
130+
result += " " + scopes[i]
131+
}
132+
return result
133+
}

0 commit comments

Comments
 (0)