mcp connection
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user