mathiasandClaude Opus 4.8 d5aa5b992c feat(auth): audit-log rejections + 503 on Dex outage (gitea-mcp#9, #6)
BearerMiddleware now emits a structured slog line on every rejection with the
reason (no_token / static_token_mismatch / jwt_invalid / jwt_dex_unavailable),
client IP (X-Forwarded-For aware), presented token type (jwt/opaque), and a
truncated SHA-256 fingerprint — never the raw token, so no secret material
reaches stdout/log aggregation.

Validate now tags a JWKS/Dex fetch failure with the exported ErrUnavailable
sentinel (distinct from a present-but-invalid token). BearerMiddleware maps it
to HTTP 503 with `WWW-Authenticate: Bearer error="temporarily_unavailable"`, so
a transient Dex outage is distinguishable from a bad token instead of a silent
generic 401.

No signature change — logging goes through slog.Default(); existing consumers
are unaffected until they set a default logger. Behavior reaches a consumer only
when it bumps the chassis version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:16:32 +02:00

mcp-chassis

Shared Go library for Mathias-owned MCP servers. Provides the auth + middleware primitives that every MCP server needs.

Why

By 2026-05-22 there were three+ MCP servers (gitea-mcp, brain-mcp / ingestion, future ones from template-go-agent) each carrying their own near-identical:

  • Dex JWT validator (~80 LOC, identical jwx/v2 plumbing)
  • Bearer middleware (~50 LOC, dual-mode static + JWT)
  • RFC 9728 protected-resource metadata handler (~25 LOC)

The homelab architecture review's spike S3 (see gitea.d-ma.be/mathias/infra/docs/superpowers/handoffs/2026-05-22-mcp-chassis-spike.md) concluded a thin shared lib pays for itself within the first migration. This is that lib.

Non-goals

  • Replacing each MCP's tool registration / handler logic — that is per-domain.
  • Solving HTTP routing — consumers keep their own http.ServeMux.
  • Solving observability — see gitea.d-ma.be/mathias/hyperguild/ingestion/internal/metrics for the hand-rolled Prometheus pattern. May absorb a metrics subpackage here later, once a second consumer needs it.

Packages

auth

  • JWTValidator — Dex OIDC JWT validation. nil is a valid value meaning "JWT auth disabled".
  • BearerMiddleware — static-Bearer-or-Dex-JWT gate. Static wins first; only emits WWW-Authenticate: Bearer ... resource_metadata=... on 401 when resourceMetadataURL is non-empty (claude.ai OAuth discovery).
  • ProtectedResourceHandler — RFC 9728 metadata document for GET /.well-known/oauth-protected-resource.

Usage

package main

import (
	"context"
	"net/http"
	"os"

	"git.d-ma.be/mathias/mcp-chassis/auth"
)

func main() {
	staticToken := os.Getenv("BRAIN_MCP_TOKEN")
	dexIssuer := os.Getenv("DEX_ISSUER_URL")
	audience := os.Getenv("MCP_AUDIENCE")
	resourceURL := os.Getenv("MCP_RESOURCE_URL")

	validator, err := auth.NewJWTValidator(context.Background(), dexIssuer, audience)
	if err != nil {
		panic(err)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /.well-known/oauth-protected-resource",
		auth.ProtectedResourceHandler(resourceURL, dexIssuer))
	mux.Handle("/mcp", auth.BearerMiddleware(
		staticToken,
		validator,
		"brain",
		resourceURL+"/.well-known/oauth-protected-resource",
		mcpHandler(),
	))

	_ = http.ListenAndServe(":3300", mux)
}

func mcpHandler() http.Handler { /* per-domain */ return nil }

Versioning

Trunk-based development on main. Tagged with semver. Consumers pin specific tags (go.mod require git.d-ma.be/mathias/mcp-chassis v0.x.y) and bump deliberately.

Migrations are documented per-consumer in the consumer's CHANGELOG / commits.

Dependencies

  • github.com/lestrrat-go/jwx/v2 — JWKS cache + JWT parsing. Same dep every MCP already had; no new transitive cost when adopting the chassis.
  • github.com/stretchr/testify — tests only.

stdlib otherwise.

S
Description
Shared Go library for Mathias-owned MCP servers — Dex JWT validator + Bearer middleware + RFC 9728 protected-resource metadata. Hand-rolled (stdlib + lestrrat-go/jwx) per the homelab architecture review.
Readme
60 KiB
Languages
Go 100%