feat(brain-mcp): OAuth 2.0 client_credentials flow for claude.ai
Adds a minimal RFC 8414 + RFC 6749 client_credentials flow so claude.ai's custom-MCP integration (no static-Bearer field in the UI) can exchange a client_id + client_secret pair for the existing BRAIN_MCP_TOKEN and use it as a Bearer on /mcp. No JWTs, no refresh, no expiry — the rest of the auth middleware is unchanged. New package ingestion/internal/oauth: - MetadataHandler(issuer): serves /.well-known/oauth-authorization-server with grant_types=[client_credentials] and both token_endpoint_auth_methods (post + basic). - TokenHandler(cfg): serves /oauth/token. Validates client_id and client_secret via constant-time compare; returns BRAIN_MCP_TOKEN as access_token. RFC 6749 §5.2 error JSON on bad grant / bad creds. Wiring in cmd/server/main.go: opt-in by setting both OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET. Setting only one is misconfiguration → exit 1. Mounts both endpoints with no auth; MCP_RESOURCE_URL supplies the issuer. Also pivots issue #8's vector backend from Qdrant to pgvector (see DECISIONS.md 2026-05-18) — Qdrant was never deployed and postgres18 with pgvector already runs as the project default; supersedes 2026-04-08 for this use case. Tests cover post-auth, basic-auth, wrong secret, bad grant, GET rejection, malformed Basic header, and Basic without colon. Closes hyperguild#5.
This commit is contained in:
38
ingestion/internal/oauth/metadata.go
Normal file
38
ingestion/internal/oauth/metadata.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Package oauth implements a minimal OAuth 2.0 client_credentials flow
|
||||
// for the brain MCP server. Designed for claude.ai's custom MCP integration
|
||||
// UI, which only supports OAuth (no static-Bearer field). The flow trades
|
||||
// a registered client_id + client_secret for the existing BRAIN_MCP_TOKEN —
|
||||
// no JWTs, no expiry, no refresh — so the rest of the auth middleware is
|
||||
// unchanged.
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MetadataHandler serves RFC 8414 authorization-server metadata at
|
||||
// GET /.well-known/oauth-authorization-server. issuer must be the public
|
||||
// origin of the brain MCP (e.g. https://brain-mcp.d-ma.be); the handler
|
||||
// derives the token endpoint from it.
|
||||
//
|
||||
// Mount with no auth — discovery must be reachable to anonymous callers.
|
||||
func MetadataHandler(issuer string) http.HandlerFunc {
|
||||
issuer = strings.TrimRight(issuer, "/")
|
||||
body, _ := json.Marshal(struct {
|
||||
Issuer string `json:"issuer"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
GrantTypes []string `json:"grant_types_supported"`
|
||||
TokenEndpointAuthMeth []string `json:"token_endpoint_auth_methods_supported"`
|
||||
}{
|
||||
Issuer: issuer,
|
||||
TokenEndpoint: issuer + "/oauth/token",
|
||||
GrantTypes: []string{"client_credentials"},
|
||||
TokenEndpointAuthMeth: []string{"client_secret_post", "client_secret_basic"},
|
||||
})
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user