feat: JWT auth middleware + /.well-known/oauth-protected-resource for supervisor and brain MCP servers (Dex OIDC) #6

Closed
opened 2026-05-11 17:22:59 +00:00 by mathias · 0 comments
Owner

Context

auth.d-ma.be now runs Dex (OIDC/OAuth 2.0 AS). All MCP servers should accept JWTs issued by Dex or a static bearer token (backward compat). Two MCP servers in this repo need updating:

  1. supervisorinternal/mcp/server.go, checkAuth function
  2. brainingestion/internal/mcp/auth.go, BearerAuth middleware

Both currently do plain static string compares. The new chain adds JWT validation before the static fallback.


Changes required

1. Supervisor — internal/mcp/server.go

Current checkAuth:

func checkAuth(token, expected string) bool {
    return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
}

New behaviour:

if token is valid JWT signed by Dex → allow
else if subtle.ConstantTimeCompare(token, SUPERVISOR_MCP_TOKEN) == 1 → allow
else → 401

Keep the existing SUPERVISOR_MCP_TOKEN env var path intact — it's the static fallback.

2. Brain — ingestion/internal/mcp/auth.go

Current BearerAuth middleware does a plain string compare. Apply the same JWT-or-static chain:

if token is valid JWT signed by Dex → allow
else if token == BRAIN_MCP_TOKEN (constant-time) → allow
else → 401

3. Shared JWT validation package

Create internal/auth/jwt.go (or equivalent shared location) implementing:

// Validator wraps a JWKS cache and validates bearer tokens.
type Validator struct { ... }

func NewValidator(issuerURL string) (*Validator, error)
func (v *Validator) Validate(ctx context.Context, rawToken string) (valid bool, subject string, err error)

Dependency: github.com/lestrrat-go/jwx/v2

New env vars (both binaries):

  • DEX_ISSUER_URL — e.g. https://auth.d-ma.be

JWKS discovery: {DEX_ISSUER_URL}/.well-known/openid-configurationjwks_uri.
Use jwk.NewCache with 1 h refresh interval.

Validation requirements:

  • iss == DEX_ISSUER_URL
  • aud contains the server's client ID (configurable, e.g. supervisor / brain)
  • exp not expired
  • Signature valid against JWKS

If DEX_ISSUER_URL is not set, skip JWT validation entirely and fall back to static token only (safe default during rollout).

4. /.well-known/oauth-protected-resource endpoints (RFC 9728)

Register on each server's HTTP mux (outside auth-protected routes):

Supervisor:

GET /.well-known/oauth-protected-resource
{
  "resource": "https://supervisor-mcp.d-ma.be",
  "authorization_servers": ["https://auth.d-ma.be"]
}

Brain:

GET /.well-known/oauth-protected-resource
{
  "resource": "https://brain-mcp.d-ma.be",
  "authorization_servers": ["https://auth.d-ma.be"]
}

Values from env vars (MCP_RESOURCE_URL, DEX_ISSUER_URL).


k3s manifest updates (in mathias/infra)

After code merged and images rebuilt:

  • k3s/apps/supervisor/deployment.yaml: add DEX_ISSUER_URL: https://auth.d-ma.be
  • k3s/apps/infra-mcp/deployment.yaml (brain): add DEX_ISSUER_URL: https://auth.d-ma.be

Note: SUPERVISOR_MCP_TOKEN enforcement is currently off in k3s (env var not set). JWT auth can be wired up independently; static token enforcement can be turned on together.


Testing

  • Unit test: mock JWKS endpoint, assert valid JWT passes and tampered/expired JWT returns 401
  • Unit test: static token passes, wrong token returns 401
  • Unit test: DEX_ISSUER_URL unset → only static token auth applies
  • Integration: GET /.well-known/oauth-protected-resource on each server returns correct JSON

  • Dex deployed at auth.d-ma.be via k3s/apps/auth/ in mathias/infra
  • Same pattern applied to mathias/gitea-mcp (separate issue there)
## Context `auth.d-ma.be` now runs Dex (OIDC/OAuth 2.0 AS). All MCP servers should accept JWTs issued by Dex **or** a static bearer token (backward compat). Two MCP servers in this repo need updating: 1. **supervisor** — `internal/mcp/server.go`, `checkAuth` function 2. **brain** — `ingestion/internal/mcp/auth.go`, `BearerAuth` middleware Both currently do plain static string compares. The new chain adds JWT validation before the static fallback. --- ## Changes required ### 1. Supervisor — `internal/mcp/server.go` Current `checkAuth`: ```go func checkAuth(token, expected string) bool { return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1 } ``` New behaviour: ``` if token is valid JWT signed by Dex → allow else if subtle.ConstantTimeCompare(token, SUPERVISOR_MCP_TOKEN) == 1 → allow else → 401 ``` Keep the existing `SUPERVISOR_MCP_TOKEN` env var path intact — it's the static fallback. ### 2. Brain — `ingestion/internal/mcp/auth.go` Current `BearerAuth` middleware does a plain string compare. Apply the same JWT-or-static chain: ``` if token is valid JWT signed by Dex → allow else if token == BRAIN_MCP_TOKEN (constant-time) → allow else → 401 ``` ### 3. Shared JWT validation package Create `internal/auth/jwt.go` (or equivalent shared location) implementing: ```go // Validator wraps a JWKS cache and validates bearer tokens. type Validator struct { ... } func NewValidator(issuerURL string) (*Validator, error) func (v *Validator) Validate(ctx context.Context, rawToken string) (valid bool, subject string, err error) ``` Dependency: `github.com/lestrrat-go/jwx/v2` New env vars (both binaries): - `DEX_ISSUER_URL` — e.g. `https://auth.d-ma.be` JWKS discovery: `{DEX_ISSUER_URL}/.well-known/openid-configuration` → `jwks_uri`. Use `jwk.NewCache` with 1 h refresh interval. Validation requirements: - `iss` == `DEX_ISSUER_URL` - `aud` contains the server's client ID (configurable, e.g. `supervisor` / `brain`) - `exp` not expired - Signature valid against JWKS If `DEX_ISSUER_URL` is not set, skip JWT validation entirely and fall back to static token only (safe default during rollout). ### 4. `/.well-known/oauth-protected-resource` endpoints (RFC 9728) Register on each server's HTTP mux (outside auth-protected routes): **Supervisor:** ``` GET /.well-known/oauth-protected-resource { "resource": "https://supervisor-mcp.d-ma.be", "authorization_servers": ["https://auth.d-ma.be"] } ``` **Brain:** ``` GET /.well-known/oauth-protected-resource { "resource": "https://brain-mcp.d-ma.be", "authorization_servers": ["https://auth.d-ma.be"] } ``` Values from env vars (`MCP_RESOURCE_URL`, `DEX_ISSUER_URL`). --- ## k3s manifest updates (in `mathias/infra`) After code merged and images rebuilt: - `k3s/apps/supervisor/deployment.yaml`: add `DEX_ISSUER_URL: https://auth.d-ma.be` - `k3s/apps/infra-mcp/deployment.yaml` (brain): add `DEX_ISSUER_URL: https://auth.d-ma.be` Note: `SUPERVISOR_MCP_TOKEN` enforcement is currently **off** in k3s (env var not set). JWT auth can be wired up independently; static token enforcement can be turned on together. --- ## Testing - Unit test: mock JWKS endpoint, assert valid JWT passes and tampered/expired JWT returns 401 - Unit test: static token passes, wrong token returns 401 - Unit test: `DEX_ISSUER_URL` unset → only static token auth applies - Integration: `GET /.well-known/oauth-protected-resource` on each server returns correct JSON --- ## Related - Dex deployed at `auth.d-ma.be` via `k3s/apps/auth/` in `mathias/infra` - Same pattern applied to `mathias/gitea-mcp` (separate issue there)
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#6