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:
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/auth"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/llm"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/mcp"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/oauth"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/pipeline"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/watcher"
|
||||
)
|
||||
@@ -128,6 +129,34 @@ func main() {
|
||||
|
||||
mux.Handle("/mcp", mcp.BearerAuth(mcpToken, jwtValidator, resourceMetadataURL, mcpSrv))
|
||||
|
||||
// Opt-in OAuth 2.0 client_credentials flow for claude.ai's custom-MCP
|
||||
// integration UI, which has no static-Bearer field. Setting both
|
||||
// OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET enables the token exchange;
|
||||
// setting only one is misconfiguration → fail fast.
|
||||
oauthID := os.Getenv("OAUTH_CLIENT_ID")
|
||||
oauthSecret := os.Getenv("OAUTH_CLIENT_SECRET")
|
||||
switch {
|
||||
case oauthID != "" && oauthSecret != "":
|
||||
issuer := os.Getenv("MCP_RESOURCE_URL")
|
||||
if issuer == "" {
|
||||
logger.Error("OAUTH_CLIENT_ID/SECRET set but MCP_RESOURCE_URL is empty; cannot derive issuer")
|
||||
os.Exit(1)
|
||||
}
|
||||
mux.HandleFunc("GET /.well-known/oauth-authorization-server",
|
||||
oauth.MetadataHandler(issuer))
|
||||
mux.HandleFunc("POST /oauth/token", oauth.TokenHandler(oauth.TokenConfig{
|
||||
ClientID: oauthID,
|
||||
ClientSecret: oauthSecret,
|
||||
AccessToken: mcpToken,
|
||||
}))
|
||||
logger.Info("oauth client_credentials enabled", "issuer", strings.TrimRight(issuer, "/"))
|
||||
case oauthID == "" && oauthSecret == "":
|
||||
// disabled — that's fine
|
||||
default:
|
||||
logger.Error("OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET must be set together")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
addr := ":" + port
|
||||
watchIntervalLog := "disabled"
|
||||
if watchInterval > 0 {
|
||||
|
||||
Reference in New Issue
Block a user