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). The current BearerMiddleware in internal/auth/bearer.go validates the bearer as a Gitea PAT via the Gitea API — that model breaks when clients authenticate via claude.ai's OAuth flow instead of supplying a PAT.
Goal
Replace the PAT-passthrough auth with a JWT-or-static chain, and expose the RFC 9728 protected resource metadata endpoint.
Current behaviour: extracts bearer token, calls GET /api/v1/user on the Gitea host, rejects if non-200.
New behaviour:
if token is valid JWT signed by Dex → allow
else if token == GITEA_MCP_STATIC_TOKEN env var (constant-time compare) → allow
else → 401 Unauthorized
JWT validation uses JWKS auto-refresh (see §3 below). Do not call the Gitea API on the hot path.
The validated token (JWT subject or static sentinel) can still be stored in context via auth.StoreTokenInContext for any downstream use.
2. internal/gitea/client.go — fix service PAT usage
doOnce and doRaw currently call auth.TokenFromContext(ctx) first and fall back to c.token. Since the caller's JWT is no longer a Gitea PAT, remove the context-token lookup. Always use c.token (the service PAT configured via GITEA_MCP_DEFAULT_TOKEN).
Register a new HTTP handler (outside the MCP router, on the root mux):
GET /.well-known/oauth-protected-resource
Content-Type: application/json
{
"resource": "https://git-mcp.d-ma.be",
"authorization_servers": ["https://auth.d-ma.be"]
}
Values should come from env vars (MCP_RESOURCE_URL, DEX_ISSUER_URL) so they're configurable.
k3s manifest updates (in mathias/infra)
After the code changes are merged and image rebuilt:
Add DEX_ISSUER_URL: https://auth.d-ma.be to k3s/apps/gitea-mcp/deployment.yaml env
Add GITEA_MCP_STATIC_TOKEN as a reference to the existing bearer secret (or a new one)
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
Integration: hit /.well-known/oauth-protected-resource, assert JSON shape
Related
Dex deployed at auth.d-ma.be via k3s/apps/auth/ in mathias/infra
Same pattern to be applied to mathias/hyperguild (supervisor + brain MCP servers)
## 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). The current `BearerMiddleware` in `internal/auth/bearer.go` validates the bearer as a Gitea PAT via the Gitea API — that model breaks when clients authenticate via claude.ai's OAuth flow instead of supplying a PAT.
## Goal
Replace the PAT-passthrough auth with a JWT-or-static chain, and expose the RFC 9728 protected resource metadata endpoint.
---
## Changes required
### 1. `internal/auth/bearer.go` — replace `BearerMiddleware`
Current behaviour: extracts bearer token, calls `GET /api/v1/user` on the Gitea host, rejects if non-200.
New behaviour:
```
if token is valid JWT signed by Dex → allow
else if token == GITEA_MCP_STATIC_TOKEN env var (constant-time compare) → allow
else → 401 Unauthorized
```
JWT validation uses JWKS auto-refresh (see §3 below). Do **not** call the Gitea API on the hot path.
The validated token (JWT subject or static sentinel) can still be stored in context via `auth.StoreTokenInContext` for any downstream use.
### 2. `internal/gitea/client.go` — fix service PAT usage
`doOnce` and `doRaw` currently call `auth.TokenFromContext(ctx)` first and fall back to `c.token`. Since the caller's JWT is no longer a Gitea PAT, remove the context-token lookup. Always use `c.token` (the service PAT configured via `GITEA_MCP_DEFAULT_TOKEN`).
```go
// Before
token := auth.TokenFromContext(ctx)
if token == "" {
token = c.token
}
// After
token := c.token
```
Verify no other callers of `auth.TokenFromContext` rely on per-request PAT injection; if they exist, update them too.
### 3. JWT library + env vars
Add dependency: `github.com/lestrrat-go/jwx/v2`
New env vars:
- `DEX_ISSUER_URL` — e.g. `https://auth.d-ma.be` (required when JWT auth is enabled)
- `GITEA_MCP_STATIC_TOKEN` — optional static bearer for backward compat / service-to-service
JWKS endpoint is `{DEX_ISSUER_URL}/.well-known/openid-configuration` → `jwks_uri`. Use `jwk.NewCache` with a reasonable refresh interval (1 h).
Validation requirements:
- `iss` == `DEX_ISSUER_URL`
- `aud` contains `gitea-mcp` (or the configured client ID)
- `exp` not expired
- Signature valid against JWKS
### 4. `/.well-known/oauth-protected-resource` endpoint (RFC 9728)
Register a new HTTP handler (outside the MCP router, on the root mux):
```
GET /.well-known/oauth-protected-resource
Content-Type: application/json
{
"resource": "https://git-mcp.d-ma.be",
"authorization_servers": ["https://auth.d-ma.be"]
}
```
Values should come from env vars (`MCP_RESOURCE_URL`, `DEX_ISSUER_URL`) so they're configurable.
---
## k3s manifest updates (in `mathias/infra`)
After the code changes are merged and image rebuilt:
- Add `DEX_ISSUER_URL: https://auth.d-ma.be` to `k3s/apps/gitea-mcp/deployment.yaml` env
- Add `GITEA_MCP_STATIC_TOKEN` as a reference to the existing bearer secret (or a new one)
---
## 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
- Integration: hit `/.well-known/oauth-protected-resource`, assert JSON shape
---
## Related
- Dex deployed at `auth.d-ma.be` via `k3s/apps/auth/` in `mathias/infra`
- Same pattern to be applied to `mathias/hyperguild` (supervisor + brain MCP servers)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context
auth.d-ma.benow runs Dex (OIDC/OAuth 2.0 AS). All MCP servers should accept JWTs issued by Dex or a static bearer token (backward compat). The currentBearerMiddlewareininternal/auth/bearer.govalidates the bearer as a Gitea PAT via the Gitea API — that model breaks when clients authenticate via claude.ai's OAuth flow instead of supplying a PAT.Goal
Replace the PAT-passthrough auth with a JWT-or-static chain, and expose the RFC 9728 protected resource metadata endpoint.
Changes required
1.
internal/auth/bearer.go— replaceBearerMiddlewareCurrent behaviour: extracts bearer token, calls
GET /api/v1/useron the Gitea host, rejects if non-200.New behaviour:
JWT validation uses JWKS auto-refresh (see §3 below). Do not call the Gitea API on the hot path.
The validated token (JWT subject or static sentinel) can still be stored in context via
auth.StoreTokenInContextfor any downstream use.2.
internal/gitea/client.go— fix service PAT usagedoOnceanddoRawcurrently callauth.TokenFromContext(ctx)first and fall back toc.token. Since the caller's JWT is no longer a Gitea PAT, remove the context-token lookup. Always usec.token(the service PAT configured viaGITEA_MCP_DEFAULT_TOKEN).Verify no other callers of
auth.TokenFromContextrely on per-request PAT injection; if they exist, update them too.3. JWT library + env vars
Add dependency:
github.com/lestrrat-go/jwx/v2New env vars:
DEX_ISSUER_URL— e.g.https://auth.d-ma.be(required when JWT auth is enabled)GITEA_MCP_STATIC_TOKEN— optional static bearer for backward compat / service-to-serviceJWKS endpoint is
{DEX_ISSUER_URL}/.well-known/openid-configuration→jwks_uri. Usejwk.NewCachewith a reasonable refresh interval (1 h).Validation requirements:
iss==DEX_ISSUER_URLaudcontainsgitea-mcp(or the configured client ID)expnot expired4.
/.well-known/oauth-protected-resourceendpoint (RFC 9728)Register a new HTTP handler (outside the MCP router, on the root mux):
Values should come from env vars (
MCP_RESOURCE_URL,DEX_ISSUER_URL) so they're configurable.k3s manifest updates (in
mathias/infra)After the code changes are merged and image rebuilt:
DEX_ISSUER_URL: https://auth.d-ma.betok3s/apps/gitea-mcp/deployment.yamlenvGITEA_MCP_STATIC_TOKENas a reference to the existing bearer secret (or a new one)Testing
/.well-known/oauth-protected-resource, assert JSON shapeRelated
auth.d-ma.beviak3s/apps/auth/inmathias/inframathias/hyperguild(supervisor + brain MCP servers)Shipped in v0.1.x. Closing during cleanup pass.