Closes #60. Closes #58 (fixed in mcp-chassis v0.7.0, consumed here). Three auth paths reach this server and none of them said so on success. PassthroughMiddleware logged nothing at all, success or failure. While verifying #59 end to end there was no way to confirm from the logs whether calls were using the new PAT pass-through — the caller's own Gitea PAT, the caller's own identity — or falling through to the shared JWT/static path. The only available reasoning was inference from which failures were absent. INFO mcp auth accepted auth_path=passthrough caller=alice token_fp=... INFO mcp auth accepted auth_path=jwt caller=mathiasbq token_fp=... INFO mcp auth accepted auth_path=static token_fp=... The first line matters more than the other two. Pass-through is the path 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 token_fp on its own accepted and rejected lines, and two different fingerprints for the same token cannot be correlated — which is the only reason the field exists. Exported from the chassis in v0.7.1 for exactly this. A token that is not a valid PAT still falls through SILENTLY. The chassis owns the verdict, and logging here as well would double-count every static-token request, since all of them reach this middleware first. Asserted, because "logs nothing on the failure path" is indistinguishable from "was never called". Also lands #58 by consuming mcp-chassis v0.7.0: a JWT presented to a server with no JWT auth configured now reports jwt_auth_not_configured rather than static_token_mismatch. That log line is what sent a debugging session into re-running the OAuth flow while the credentials were valid the whole time. Verified: task check EXIT=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.4 KiB
Go
63 lines
2.4 KiB
Go
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)
|
|
})
|
|
}
|