package mcp import ( "crypto/subtle" "net/http" "strings" "github.com/mathiasbq/hyperguild/ingestion/internal/auth" ) // BearerAuth gates an HTTP handler behind dual-mode authentication. // // Auth precedence: // // 1. Static Bearer match (constant-time compare against staticToken). // Wins immediately and never emits a WWW-Authenticate header. This is // the path used by internal Tailscale/LAN CLI callers that supply // `Authorization: Bearer $BRAIN_MCP_TOKEN` via `.mcp.json`. Returning // 200 without a WWW-Authenticate prevents the MCP client from // speculatively flipping into OAuth-discovery mode. // 2. Dex JWT validation (when validator is non-nil). Used by claude.ai // custom MCP connectors that finished the OAuth handshake. // 3. Otherwise 401. When resourceMetadataURL is non-empty, a // `WWW-Authenticate: Bearer resource_metadata="…"` header is emitted // per RFC 9728 §6.2 so claude.ai's OAuth discovery flow can find the // server's protected-resource metadata document. // // The order matters: a valid static Bearer must short-circuit BEFORE any // JWT path runs, because a non-empty WWW-Authenticate emitted on the // fall-through 401 confuses static-Bearer-only clients into discarding // their header and starting an OAuth handshake instead. func BearerAuth(staticToken string, validator *auth.Validator, resourceMetadataURL string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rawToken, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !ok { unauthorized(w, resourceMetadataURL) return } // 1. Static Bearer wins first — never emits a challenge. if staticToken != "" && subtle.ConstantTimeCompare([]byte(rawToken), []byte(staticToken)) == 1 { next.ServeHTTP(w, r) return } // 2. Then Dex JWT, if configured. if validator != nil { if _, err := validator.Validate(r.Context(), rawToken); err == nil { next.ServeHTTP(w, r) return } } // 3. Reject with an OAuth resource-metadata challenge if configured. unauthorized(w, resourceMetadataURL) }) } func unauthorized(w http.ResponseWriter, resourceMetadataURL string) { if resourceMetadataURL != "" { w.Header().Set("WWW-Authenticate", `Bearer realm="brain", resource_metadata="`+resourceMetadataURL+`"`) } http.Error(w, "unauthorized", http.StatusUnauthorized) }