Files
Adam Holt efe9d40b58
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
Token scopes context (#1997)
* Move scope storage into its own context key, separately from token info.

This allows us to provide scopes seperately in the remote server, where
we have scopes before we do the auth.

* Skip token extraction if token info already exists in context.

This is to avoid redundant token extraction in remote setup where token info may have already been extracted earlier in the request lifecycle.

* Check for existing scopes in context before fetching from GitHub API in scope challenge middleware

* Return error type for unknown tools in inventory builder and handle it in HTTP handler
2026-02-16 14:10:28 +01:00

57 lines
1.9 KiB
Go

package middleware
import (
"errors"
"fmt"
"net/http"
ghcontext "github.com/github/github-mcp-server/pkg/context"
"github.com/github/github-mcp-server/pkg/http/oauth"
"github.com/github/github-mcp-server/pkg/utils"
)
func ExtractUserToken(oauthCfg *oauth.Config) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Check if token info already exists in context, if it does, skip extraction.
// In remote setup, we may have already extracted token info earlier.
if _, ok := ghcontext.GetTokenInfo(ctx); ok {
// Token info already exists in context, skip extraction
next.ServeHTTP(w, r)
return
}
tokenType, token, err := utils.ParseAuthorizationHeader(r)
if err != nil {
// For missing Authorization header, return 401 with WWW-Authenticate header per MCP spec
if errors.Is(err, utils.ErrMissingAuthorizationHeader) {
sendAuthChallenge(w, r, oauthCfg)
return
}
// For other auth errors (bad format, unsupported), return 400
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = ghcontext.WithTokenInfo(ctx, &ghcontext.TokenInfo{
Token: token,
TokenType: tokenType,
})
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
// sendAuthChallenge sends a 401 Unauthorized response with WWW-Authenticate header
// containing the OAuth protected resource metadata URL as per RFC 6750 and MCP spec.
func sendAuthChallenge(w http.ResponseWriter, r *http.Request, oauthCfg *oauth.Config) {
resourcePath := oauth.ResolveResourcePath(r, oauthCfg)
resourceMetadataURL := oauth.BuildResourceMetadataURL(r, oauthCfg, resourcePath)
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer resource_metadata=%q`, resourceMetadataURL))
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}