package auth import ( "context" "log/slog" "net/http" "strings" chassisauth "git.d-ma.be/mathias/mcp-chassis/auth" "git.d-ma.be/mathias/gitea-mcp/internal/gitea" ) // TokenValidator asks the upstream service who a bearer token belongs to. type TokenValidator interface { ValidateToken(ctx context.Context, token string) (username string, ok bool) } // PassthroughMiddleware lets a caller authenticate with their own Gitea PAT: // if the request's bearer token validates directly against Gitea, it's used // as-is for every upstream call this request makes (gitea-mcp#59), instead of // the server's shared default token. Any other bearer (static token, JWT, or // none) falls through to fallback unchanged — this only adds a capability, it // never removes the existing auth paths. func PassthroughMiddleware(validator TokenValidator, onValid, fallback http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authz := r.Header.Get("Authorization") token, hasBearer := strings.CutPrefix(authz, "Bearer ") if hasBearer && token != "" { if login, ok := validator.ValidateToken(r.Context(), token); ok { // gitea-mcp#60. This middleware logged nothing at all, so there // was no way to tell a PAT pass-through request from one that // fell through to the shared JWT/static path — only inference // from which failures were absent. // // It matters more here than on the other two paths: this is // where a request acts as a SPECIFIC Gitea user. An audit trail // that cannot say which identity performed a write is not an // audit trail. // // chassisauth.Fingerprint, not a local equivalent: the chassis // emits the same field on its own accepted/rejected lines, and // two different fingerprints for one token cannot be correlated. slog.Info("mcp auth accepted", "auth_path", "passthrough", "caller", login, "token_fp", chassisauth.Fingerprint(token), "path", r.URL.Path, ) ctx := withCaller(r.Context(), login) ctx = gitea.WithToken(ctx, token) onValid.ServeHTTP(w, r.WithContext(ctx)) return } // A token that is not a valid PAT falls through SILENTLY. The // chassis owns the verdict; logging here too would double-count // every static-token request, since all of them reach this // middleware first. } fallback.ServeHTTP(w, r) }) }