Previously BearerMiddleware allowed requests with no Authorization header to pass through whenever GITEA_MCP_DEFAULT_TOKEN was set. The intent was "fall back to the service PAT for upstream Gitea calls," but the side effect was that anyone could hit /mcp anonymously and the server would happily proxy requests as the service account. Drop that path. Auth on /mcp now requires either: - a valid Dex-issued JWT, or - a Bearer matching GITEA_MCP_STATIC_TOKEN. The Gitea service PAT (GITEA_MCP_DEFAULT_TOKEN) is no longer wired into BearerMiddleware at all — it stays an upstream-client concern, used by gitea.NewClient for outbound API calls only. This decouples "can this caller invoke a tool" from "what credentials does the tool use against Gitea". Tests updated: drop the NoAuthHeader_WithDefault permissive case, add NoAuthHeader_RejectsEvenWhenStaticConfigured to lock in the new behavior. Closes part of mathias/infra#2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43 lines
1.3 KiB
Go
43 lines
1.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// BearerMiddleware authenticates requests via the Authorization header.
|
|
//
|
|
// A request is allowed when:
|
|
//
|
|
// 1. The Bearer token is a valid JWT issued by the configured Dex OIDC server, or
|
|
// 2. The Bearer token matches staticToken (constant-time compare).
|
|
//
|
|
// Any other case — including missing or empty Authorization header — returns 401.
|
|
//
|
|
// The Gitea service PAT is intentionally NOT used to authenticate the caller:
|
|
// it is only used by the Gitea client for upstream API calls. Decoupling the
|
|
// two prevents the MCP endpoint from being reachable anonymously when a service
|
|
// PAT happens to be configured.
|
|
func BearerMiddleware(jwtValidator *JWTValidator, staticToken string, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
bearer, hasBearer := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
|
|
if !hasBearer || bearer == "" {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if jwtValidator.Validate(r.Context(), bearer) {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
if staticToken != "" && subtle.ConstantTimeCompare([]byte(bearer), []byte(staticToken)) == 1 {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
})
|
|
}
|