package auth import ( "crypto/subtle" "net/http" "strings" ) // BearerMiddleware gates next behind dual-mode authentication. It is the // canonical pattern across every Mathias-owned MCP server. // // 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 CLI callers that supply // `Authorization: Bearer $XXX_MCP_TOKEN` via `.mcp.json`. Returning // 401 without a WWW-Authenticate prevents the MCP client from // speculatively flipping into OAuth-discovery mode and discarding // the static token. // 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 realm="", 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 the // JWT path runs, because the WWW-Authenticate emitted on the fall-through // 401 confuses static-Bearer-only clients into discarding their header // and starting an OAuth handshake instead. // // staticToken may be empty (static auth disabled — only JWT accepted). // validator may be nil (JWT auth disabled — only static accepted). // realm is a free-text identifier used in the WWW-Authenticate challenge; // MCP servers conventionally use their service name (e.g. "brain", "gitea"). func BearerMiddleware( staticToken string, validator *JWTValidator, realm string, 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 || rawToken == "" { unauthorized(w, realm, 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, realm, resourceMetadataURL) }) } func unauthorized(w http.ResponseWriter, realm, resourceMetadataURL string) { if resourceMetadataURL != "" { w.Header().Set("WWW-Authenticate", `Bearer realm="`+realm+`", resource_metadata="`+resourceMetadataURL+`"`) } http.Error(w, "unauthorized", http.StatusUnauthorized) }