mcp connection
Some checks failed
CI / fixture (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
2026-09-22 10:58:02 +05:30
parent 4e1f746b22
commit f2aa3b3ad8
53 changed files with 12515 additions and 37 deletions

View File

@@ -206,7 +206,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
// which is wider, stops one host working through many accounts. They are
// separate limiters because they are deliberately different sizes — see the
// note on Server.
addr := clientAddr(r)
addr := s.trust.clientAddr(r)
emailKey := strings.ToLower(email)
for _, check := range []struct {
limiter *attemptLimiter
@@ -318,6 +318,85 @@ var publicPaths = map[string]bool{
"/health": true,
"/api/v1/auth/login": true,
"/api/v1/auth/logout": true,
// ── The OAuth surface for MCP clients ──────────────────────────────────
//
// Four paths, each public for a specific reason rather than because
// "/oauth/*" is convenient. The namespace is deliberately NOT wildcarded:
// /oauth/authorize is not here, because it renders a consent screen for a
// signed-in person and must keep requiring a session.
//
// These routes are registered only when OAUTH_ISSUER and MCP_RESOURCE are
// configured. Listing them here is harmless otherwise — an unregistered
// path still 404s, it simply does so without being asked for a cookie.
// RFC 9728 and RFC 8414. A client with no token cannot read a document
// that requires one, and these are how it discovers where to get a token.
// They contain public endpoint URLs and nothing else.
"/.well-known/oauth-protected-resource": true,
"/.well-known/oauth-authorization-server": true,
// RFC 7591. A client that has never registered has no credential to
// present; that is what dynamic registration is for.
"/oauth/register": true,
// The client authenticates here with an authorization code or a refresh
// token in the BODY. This is a back-channel call from the MCP client's own
// servers — there is no browser and no cookie to send.
"/oauth/token": true,
// Revocation authenticates by presenting the token being revoked, for the
// same back-channel reason.
"/oauth/revoke": true,
// /mcp is listed here, and it is the entry that most deserves explaining,
// because "public" is the opposite of what it means for this path.
//
// The MCP endpoint authenticates its OWN callers, from the Authorization
// header, inside mcpserver — every method but the handshake requires a
// valid bearer token, and the transport ignores whatever identity this
// middleware may have put in the context. So listing it here does not make
// it reachable without a credential; it makes THIS middleware step aside
// so the one that knows how to answer can.
//
// It has to step aside. An MCP client discovers how to authenticate by
// calling the endpoint with no token and reading the WWW-Authenticate
// header of the 401 — RFC 9728, and the first step of the whole flow.
// This middleware's 401 carries no such header, so guarding /mcp here
// would mean a client received a refusal with nowhere to go and the
// connection could never be established. That is not a hypothetical: it is
// what TestMCPWithoutBearerReturns401AndDiscoveryPointer caught.
//
// What stops a cookie authenticating an MCP call is therefore NOT this
// allowlist — it is mcpserver taking its identity as a parameter rather
// than from the request context. See mcpserver/auth.go, and
// TestMCPRejectsACookieSession below.
"/mcp": true,
// /oauth/authorize is here for the same reason as /mcp, and it took a live
// client to show why.
//
// It was withheld on the reasoning that consent needs a signed-in person,
// so the route "genuinely wants the cookie". That reasoning was right about
// the requirement and wrong about who enforces it. THE HANDLER already
// enforces it — authserver.go asks sessions.CurrentUser, refuses to render
// consent without an identity, and redirects an anonymous visitor to the
// login with the authorization request preserved in returnTo. Guarding the
// path HERE meant that handler was never reached, so the redirect it
// performs could never run: every signed-out visitor got this middleware's
// JSON 401 instead of a login page.
//
// That is not a cosmetic difference. A first-time connector user is signed
// out by definition, so OAuth's browser leg was unreachable for exactly the
// people who needed it. Claude Web stopped here — discovery, registration,
// then a 401 with nowhere to go. Claude Desktop only got past it because a
// session had been established by hand beforehand.
//
// Listing it grants nothing: no session still means no consent screen and
// no authorization code, and the consent POST still requires the
// session-bound CSRF token. What changes is only WHICH layer says no, and
// therefore whether it can say "sign in here" instead of "no".
"/oauth/authorize": true,
}
// authenticate resolves the session cookie into an identity, or refuses.