package oauth import ( "crypto/subtle" "encoding/json" "net/http" ) // TokenConfig is the static configuration for the token endpoint. All // three fields are required. type TokenConfig struct { // ClientID and ClientSecret are the single accepted credentials. // claude.ai's custom-MCP UI persists these on its side. ClientID string ClientSecret string // AccessToken is the bearer value handed back on a successful // exchange. In this deployment it is BRAIN_MCP_TOKEN — the same // static token the rest of the auth middleware already accepts — // so no JWT machinery is needed downstream. AccessToken string } // TokenHandler serves POST /oauth/token. Implements the // client_credentials grant only, with client_secret_post and // client_secret_basic auth methods (both advertised by MetadataHandler). // Errors follow RFC 6749 §5.2 — JSON body with an "error" field. // // Mount with no auth — credentials live in the request body / header. func TokenHandler(cfg TokenConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) writeOAuthError(w, http.StatusMethodNotAllowed, "invalid_request", "POST required") return } if err := r.ParseForm(); err != nil { writeOAuthError(w, http.StatusBadRequest, "invalid_request", "form parse") return } if r.PostForm.Get("grant_type") != "client_credentials" { writeOAuthError(w, http.StatusBadRequest, "unsupported_grant_type", "only client_credentials is supported") return } clientID, clientSecret := extractClientCreds(r) if !constantTimeEqual(clientID, cfg.ClientID) || !constantTimeEqual(clientSecret, cfg.ClientSecret) { writeOAuthError(w, http.StatusUnauthorized, "invalid_client", "bad credentials") return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` }{cfg.AccessToken, "bearer"}) } } // extractClientCreds returns the client_id and client_secret pair from // either client_secret_basic (HTTP Basic) or client_secret_post (form // fields). When both are present, Basic wins per RFC 6749 §2.3.1. func extractClientCreds(r *http.Request) (string, string) { if id, secret, ok := r.BasicAuth(); ok { return id, secret } return r.PostForm.Get("client_id"), r.PostForm.Get("client_secret") } func constantTimeEqual(a, b string) bool { if a == "" || b == "" { return false } return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 } func writeOAuthError(w http.ResponseWriter, status int, code, desc string) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(struct { Error string `json:"error"` ErrorDescription string `json:"error_description,omitempty"` }{code, desc}) }