5 Commits
Author SHA1 Message Date
mathiasandClaude Opus 4.8 4e16913248 feat(auth): per-issuer HTTPClient for authenticated JWKS fetch (infra ADR-0011)
The k3s OIDC discovery + JWKS endpoints require an AUTHENTICATED request
(anonymous -> 401; anonymous-auth is off on the cluster) and are served over the
cluster CA. A bare http.DefaultClient (correct for public Authentik) cannot
reach them, so multi-issuer alone (v0.4.0) could not actually validate k8s SA
tokens in-cluster.

Add IssuerConfig.HTTPClient: when set, it fetches THAT issuer's discovery + JWKS
(threaded into discoverJWKSURI and jwk.Cache.Register via jwk.WithHTTPClient).
The k8s-issuer consumer supplies a client that trusts the cluster CA
(/var/run/secrets/kubernetes.io/serviceaccount/ca.crt) and carries the server
pod's SA bearer. nil keeps http.DefaultClient (Authentik/public, unchanged).

Test: an auth-gated in-process issuer — unauthenticated fetch fails, a client
carrying the credential builds + validates a token. Empirically grounded in the
k3s 401 finding during the ADR-0011 in-cluster PoC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.5.0
2026-07-06 23:25:04 +02:00
mathiasandClaude Opus 4.8 75eb8b831c feat(auth): multi-issuer JWT validation — accept k8s SA tokens (infra ADR-0011)
JWTValidator trusted a single issuer (iss matched exactly against DEX_ISSUER_URL),
which is why every in-cluster caller fell back to a static bearer. Add support
for a LIST of trusted issuers so one MCP server can accept Authentik JWTs
(interactive/web) AND k3s cluster ServiceAccount tokens (in-cluster, audience-
bound, kubelet-rotated) at once — the enabler for ADR-0011.

- Add IssuerConfig{IssuerURL, Audience} + NewMultiJWTValidator([]IssuerConfig).
- Refactor JWTValidator to hold []issuerEntry + a shared jwk.Cache; Validate
  tries each trusted issuer, returns the subject on first success. All-issuers-
  JWKS-unreachable -> ErrUnavailable (503); any definitive reject -> 401.
- NewJWTValidator (single issuer) and BearerMiddleware signatures UNCHANGED —
  existing consumers (gitea-mcp, ingestion) compile and behave identically.
- Add auth/jwt_test.go: an in-process OIDC issuer harness (discovery + JWKS +
  token minting) — the chassis previously had NO happy-path JWT test. Covers
  accept-either-trusted-issuer, reject-untrusted (401 not 503), per-issuer
  audience enforcement, single-issuer backward compat.

Proven viable by the ADR-0011 live-cluster spike (k3s OIDC/JWKS validates a
projected SA token). Follow-up: wire gitea-mcp/brain-mcp/ingestion to also trust
the k3s issuer + mount an audience-scoped projected SA token on one pod.

Refs infra ADR-0011; supersedes the sidecar in infra#183. (Tracked as
hyperguild#77, which was misfiled — the chassis is this repo, not hyperguild.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.4.0
2026-07-06 23:14:56 +02:00
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>
v0.3.0
2026-07-03 23:16:32 +02:00
mathiasandClaude Opus 4.8 69d389d3f7 chore(module): rename gitea.d-ma.be → git.d-ma.be module path
The host rename broke `go mod download` for consumers: the server serves a
go-import meta tag of git.d-ma.be/... which no longer matches the old
gitea.d-ma.be/... import path (hyperguild #74). Rename the module path to
match. Single leaf package (auth/), no internal self-imports — go.mod +
README example only. Tagged v0.2.0; v0.1.0 (old path) left intact for
unmigrated consumers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.2.0
2026-06-30 21:34:18 +02:00
mathiasandClaude Opus 4.7 67decddc8a feat: initial mcp-chassis with auth primitives
Shared Go library for Mathias-owned MCP servers, born from spike S3 of
the 2026-05 homelab architecture review (see
gitea.d-ma.be/mathias/infra/docs/superpowers/handoffs/2026-05-22-mcp-chassis-spike.md
for the viability assessment and abort-criterion check).

Provides three primitives every MCP server today re-implements:

- auth.JWTValidator — Dex OIDC JWT validation. nil-safe (nil = "JWT
  disabled"), audience-optional. Lifted from the identical
  ~80-LOC implementations in gitea-mcp and hyperguild/ingestion.
- auth.BearerMiddleware — dual-mode static-Bearer-or-Dex-JWT gate.
  Static wins first to avoid emitting a WWW-Authenticate challenge
  that would flip claude.ai's MCP client into OAuth discovery for
  static-only deployments. The fall-through 401 emits the RFC 9728
  resource_metadata header only when explicitly configured.
- auth.ProtectedResourceHandler — RFC 9728
  /.well-known/oauth-protected-resource metadata document handler.

Test coverage exercises every branch (static OK, JWT-disabled,
empty bearer, wrong static, with-challenge vs without-challenge,
nil-validator-Validate). go test -race clean.

Deps: github.com/lestrrat-go/jwx/v2 (already a dep of every consumer)
and testify (test-only). No new transitive deps.

First migration target: gitea-mcp. If that port lands in one PR
(abort criterion from spec), brain-mcp (ingestion) follows. Otherwise
chassis reverts per the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.1.0
2026-05-22 09:07:53 +02:00