Skip to content

Commit 9ab8046

Browse files
refactor(oauth): address review — omit empty bearer header, guard token/oauth
- BearerAuthTransport omits the Authorization header entirely when the token is empty (pre-authorization) rather than sending an empty "Bearer " value. - RunStdioServer rejects the ambiguous combination of a static Token and an OAuthManager up front, enforcing the documented mutual exclusivity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 42015ed commit 9ab8046

4 files changed

Lines changed: 31 additions & 4 deletions

File tree

internal/ghmcp/oauth_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,22 @@ func TestCreateOAuthMiddleware(t *testing.T) {
286286
})
287287
}
288288

289+
// TestRunStdioServerRejectsTokenAndOAuth verifies the mutually-exclusive guard:
290+
// supplying both a static token and an OAuth manager is rejected before the
291+
// server starts, rather than silently preferring one for auth and the other for
292+
// scope filtering.
293+
func TestRunStdioServerRejectsTokenAndOAuth(t *testing.T) {
294+
t.Parallel()
295+
296+
mgr := oauth.NewManager(oauth.NewGitHubConfig("client-id", "", nil, "", 0), discardLogger())
297+
err := RunStdioServer(StdioServerConfig{
298+
Token: "ghp_static",
299+
OAuthManager: mgr,
300+
})
301+
require.Error(t, err)
302+
assert.Contains(t, err.Error(), "mutually exclusive")
303+
}
304+
289305
// TestCreateGitHubClientsTokenProvider proves the OAuth wiring: when a
290306
// TokenProvider is configured the REST client authenticates with the provider's
291307
// current token on every request (and never pins a stale one), which is what the
@@ -317,7 +333,7 @@ func TestCreateGitHubClientsTokenProvider(t *testing.T) {
317333
}
318334

319335
do()
320-
assert.Equal(t, "Bearer", gotAuth, "no token before authorization")
336+
assert.Equal(t, "", gotAuth, "no auth header before authorization")
321337

322338
current = "oauth-token"
323339
do()

internal/ghmcp/server.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,13 @@ type StdioServerConfig struct {
261261

262262
// RunStdioServer is not concurrent safe.
263263
func RunStdioServer(cfg StdioServerConfig) error {
264+
// OAuth login and a static token are mutually exclusive: they would
265+
// disagree on how the token is sourced (lazy provider vs. static) and on
266+
// scope filtering, so reject the ambiguous combination up front.
267+
if cfg.OAuthManager != nil && cfg.Token != "" {
268+
return fmt.Errorf("OAuthManager and a static Token are mutually exclusive: provide one or the other")
269+
}
270+
264271
// Create app context
265272
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
266273
defer stop()

pkg/http/transport/bearer.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ func (t *BearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro
2525
if t.TokenProvider != nil {
2626
token = t.TokenProvider()
2727
}
28-
req.Header.Set(headers.AuthorizationHeader, "Bearer "+token)
28+
// Before OAuth authorization completes the token is empty; send an
29+
// unauthenticated request rather than an empty "Bearer " header.
30+
if token != "" {
31+
req.Header.Set(headers.AuthorizationHeader, "Bearer "+token)
32+
}
2933

3034
// Check for GraphQL-Features in context and add header if present
3135
if features := ghcontext.GetGraphQLFeatures(req.Context()); len(features) > 0 {

pkg/http/transport/bearer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestBearerAuthTransport(t *testing.T) {
4141
{
4242
name: "token provider may return empty before authorization",
4343
tokenProvider: func() string { return "" },
44-
wantAuth: "Bearer",
44+
wantAuth: "",
4545
},
4646
}
4747

@@ -103,7 +103,7 @@ func TestBearerAuthTransport_TokenProviderResolvedPerRequest(t *testing.T) {
103103
}
104104

105105
do()
106-
assert.Equal(t, "Bearer", gotAuth, "no token yet before authorization")
106+
assert.Equal(t, "", gotAuth, "no auth header before authorization")
107107

108108
current = "first-token"
109109
do()

0 commit comments

Comments
 (0)