Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
refactor(oauth): address review — omit empty bearer header, guard tok…
…en/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>
  • Loading branch information
SamMorrowDrums and Copilot committed Jun 18, 2026
commit 622d429004373b3f508ed1ccbef164a1c5973e84
18 changes: 17 additions & 1 deletion internal/ghmcp/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,22 @@ func TestCreateOAuthMiddleware(t *testing.T) {
})
}

// TestRunStdioServerRejectsTokenAndOAuth verifies the mutually-exclusive guard:
// supplying both a static token and an OAuth manager is rejected before the
// server starts, rather than silently preferring one for auth and the other for
// scope filtering.
func TestRunStdioServerRejectsTokenAndOAuth(t *testing.T) {
t.Parallel()

mgr := oauth.NewManager(oauth.NewGitHubConfig("client-id", "", nil, "", 0), discardLogger())
err := RunStdioServer(StdioServerConfig{
Token: "ghp_static",
OAuthManager: mgr,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "mutually exclusive")
}

// TestCreateGitHubClientsTokenProvider proves the OAuth wiring: when a
// TokenProvider is configured the REST client authenticates with the provider's
// current token on every request (and never pins a stale one), which is what the
Expand Down Expand Up @@ -317,7 +333,7 @@ func TestCreateGitHubClientsTokenProvider(t *testing.T) {
}
Comment on lines +358 to +379

do()
assert.Equal(t, "Bearer", gotAuth, "no token before authorization")
assert.Equal(t, "", gotAuth, "no auth header before authorization")

current = "oauth-token"
do()
Expand Down
7 changes: 7 additions & 0 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ type StdioServerConfig struct {

// RunStdioServer is not concurrent safe.
func RunStdioServer(cfg StdioServerConfig) error {
// OAuth login and a static token are mutually exclusive: they would
// disagree on how the token is sourced (lazy provider vs. static) and on
// scope filtering, so reject the ambiguous combination up front.
if cfg.OAuthManager != nil && cfg.Token != "" {
return fmt.Errorf("OAuthManager and a static Token are mutually exclusive: provide one or the other")
}

// Create app context
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
Expand Down
6 changes: 5 additions & 1 deletion pkg/http/transport/bearer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ func (t *BearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro
if t.TokenProvider != nil {
token = t.TokenProvider()
}
req.Header.Set(headers.AuthorizationHeader, "Bearer "+token)
// Before OAuth authorization completes the token is empty; send an
// unauthenticated request rather than an empty "Bearer " header.
if token != "" {
req.Header.Set(headers.AuthorizationHeader, "Bearer "+token)
}

// Check for GraphQL-Features in context and add header if present
if features := ghcontext.GetGraphQLFeatures(req.Context()); len(features) > 0 {
Expand Down
4 changes: 2 additions & 2 deletions pkg/http/transport/bearer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestBearerAuthTransport(t *testing.T) {
{
name: "token provider may return empty before authorization",
tokenProvider: func() string { return "" },
wantAuth: "Bearer",
wantAuth: "",
},
}

Expand Down Expand Up @@ -103,7 +103,7 @@ func TestBearerAuthTransport_TokenProviderResolvedPerRequest(t *testing.T) {
}
Comment on lines +84 to +103

do()
assert.Equal(t, "Bearer", gotAuth, "no token yet before authorization")
assert.Equal(t, "", gotAuth, "no auth header before authorization")

current = "first-token"
do()
Expand Down