When Dex is unreachable at startup, NewJWTValidator returns nil, nil and emits a warning. All subsequent JWT validation calls silently return false, falling through to the static token check. The resulting 401 gives the caller no indication of whether their JWT was invalid or Dex was unavailable.
Files:
cmd/gitea-mcp/main.go:30-33 — silent nil return on Dex failure
internal/auth/jwt.go:61-63 — Validate returns false when validator is nil
Suggestions:
Expose a GET /healthz that reports JWT validator status (enabled/disabled/last-error)
Log Dex availability state on each auth attempt when the validator is nil so ops can correlate with Dex outage windows
Consider returning a 503 with WWW-Authenticate: Bearer error="temporarily_unavailable" when Dex is known-down vs a generic 401
When Dex is unreachable at startup, `NewJWTValidator` returns `nil, nil` and emits a warning. All subsequent JWT validation calls silently return `false`, falling through to the static token check. The resulting 401 gives the caller no indication of whether their JWT was invalid or Dex was unavailable.
**Files:**
- `cmd/gitea-mcp/main.go:30-33` — silent nil return on Dex failure
- `internal/auth/jwt.go:61-63` — Validate returns false when validator is nil
**Suggestions:**
- Expose a `GET /healthz` that reports JWT validator status (enabled/disabled/last-error)
- Log Dex availability state on each auth attempt when the validator is nil so ops can correlate with Dex outage windows
- Consider returning a `503` with `WWW-Authenticate: Bearer error="temporarily_unavailable"` when Dex is known-down vs a generic 401
Finding during scope eval: cited file internal/auth/jwt.go:61-63 is gone — commit 658f4ba migrated the JWT validator and Bearer middleware to the external mcp-chassis v0.1.0 module (gitea.d-ma.be/mathias/mcp-chassis/auth). The silent-degradation behaviour persists post-migration but the code lives in two repos now.
What this commit covers (gitea-mcp scope):
/healthz now emits JSON with three-state JWT status: disabled (DEX_ISSUER_URL unset), enabled (validator initialized), degraded (configured but init failed — only static-token auth currently accepted).
last_error surfaces the NewJWTValidator error string so ops can correlate degraded with Dex outage windows without tailing pod logs.
Tests cover all three states (cmd/gitea-mcp/healthz_test.go).
Deferred to follow-up (mcp-chassis scope):
Per-attempt log of Dex validator state when validator == nil — lives in BearerMiddleware, would log on every request that hits the chassis (mcp-chassis/auth/bearer.go:59-64).
503 with WWW-Authenticate: Bearer error="temporarily_unavailable" when Dex is known-down — Validate would need to distinguish "validator not configured" from "validator unhealthy"; touches the auth contract for every MCP consumer (gitea-mcp, brain-mcp, future template-go-agent spawn).
Both items require coordinated changes in chassis + a v0.1.1 tag + go.mod bumps in every consumer. Worth opening a chassis-side issue. Leaving this one open until that's done; close when chassis ships.
Partial fix landed in 668e8fa.
**Finding during scope eval:** cited file `internal/auth/jwt.go:61-63` is gone — commit 658f4ba migrated the JWT validator and Bearer middleware to the external `mcp-chassis` v0.1.0 module (`gitea.d-ma.be/mathias/mcp-chassis/auth`). The silent-degradation behaviour persists post-migration but the code lives in two repos now.
**What this commit covers (gitea-mcp scope):**
- `/healthz` now emits JSON with three-state JWT status: `disabled` (DEX_ISSUER_URL unset), `enabled` (validator initialized), `degraded` (configured but init failed — only static-token auth currently accepted).
- `last_error` surfaces the `NewJWTValidator` error string so ops can correlate `degraded` with Dex outage windows without tailing pod logs.
- Tests cover all three states (`cmd/gitea-mcp/healthz_test.go`).
**Deferred to follow-up (mcp-chassis scope):**
1. Per-attempt log of Dex validator state when `validator == nil` — lives in `BearerMiddleware`, would log on every request that hits the chassis (`mcp-chassis/auth/bearer.go:59-64`).
2. `503` with `WWW-Authenticate: Bearer error="temporarily_unavailable"` when Dex is known-down — `Validate` would need to distinguish "validator not configured" from "validator unhealthy"; touches the auth contract for every MCP consumer (gitea-mcp, brain-mcp, future template-go-agent spawn).
Both items require coordinated changes in chassis + a v0.1.1 tag + `go.mod` bumps in every consumer. Worth opening a chassis-side issue. Leaving this one open until that's done; close when chassis ships.
Already shipped in gitea-mcp (present before this pass): /healthz reports the JWT validator state — disabled (DEX_ISSUER_URL unset) / enabled / degraded (configured but init failed) with last_error — and startup logs a warning when the validator fails to initialize. That covers the "silent degradation is now observable" asks (newHealthzHandler, TestHealthzHandler).
New in mcp-chassis v0.3.0 (adopted by gitea-mcp v0.5.0): the runtime Dex-outage case is now distinct from a bad token. JWTValidator.Validate tags a JWKS/Dex fetch failure with the exported ErrUnavailable sentinel, and BearerMiddleware maps it to HTTP 503 with WWW-Authenticate: Bearer error="temporarily_unavailable" (RFC 6750 §3.1) instead of a generic 401 — so a caller can tell "Dex is down, retry" from "your token is invalid". Covered by TestValidate_JWKSUnreachable_IsUnavailable + TestBearerMiddleware_DexDown_503.
Scope note: the 503 fires for a runtime JWKS-fetch failure. The startup-degraded case (validator nil because Dex was down at boot) still returns 401 at the middleware, because BearerMiddleware receives a nil validator and can't distinguish "JWT disabled" from "degraded" without a signature change across all chassis consumers — /healthz surfaces that state instead. If you want startup-degraded to also emit 503, that's a follow-up chassis signature change; noting rather than silently doing it.
Closed across two layers.
**Already shipped in gitea-mcp** (present before this pass): `/healthz` reports the JWT validator state — `disabled` (DEX_ISSUER_URL unset) / `enabled` / `degraded` (configured but init failed) with `last_error` — and startup logs a warning when the validator fails to initialize. That covers the "silent degradation is now observable" asks (`newHealthzHandler`, `TestHealthzHandler`).
**New in mcp-chassis v0.3.0** (adopted by gitea-mcp v0.5.0): the runtime Dex-outage case is now distinct from a bad token. `JWTValidator.Validate` tags a JWKS/Dex fetch failure with the exported `ErrUnavailable` sentinel, and `BearerMiddleware` maps it to **HTTP 503 with `WWW-Authenticate: Bearer error="temporarily_unavailable"`** (RFC 6750 §3.1) instead of a generic 401 — so a caller can tell "Dex is down, retry" from "your token is invalid". Covered by `TestValidate_JWKSUnreachable_IsUnavailable` + `TestBearerMiddleware_DexDown_503`.
**Scope note:** the 503 fires for a *runtime* JWKS-fetch failure. The *startup-degraded* case (validator nil because Dex was down at boot) still returns 401 at the middleware, because `BearerMiddleware` receives a nil validator and can't distinguish "JWT disabled" from "degraded" without a signature change across all chassis consumers — `/healthz` surfaces that state instead. If you want startup-degraded to also emit 503, that's a follow-up chassis signature change; noting rather than silently doing it.
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.
When Dex is unreachable at startup,
NewJWTValidatorreturnsnil, niland emits a warning. All subsequent JWT validation calls silently returnfalse, falling through to the static token check. The resulting 401 gives the caller no indication of whether their JWT was invalid or Dex was unavailable.Files:
cmd/gitea-mcp/main.go:30-33— silent nil return on Dex failureinternal/auth/jwt.go:61-63— Validate returns false when validator is nilSuggestions:
GET /healthzthat reports JWT validator status (enabled/disabled/last-error)503withWWW-Authenticate: Bearer error="temporarily_unavailable"when Dex is known-down vs a generic 401Partial fix landed in
668e8fa.Finding during scope eval: cited file
internal/auth/jwt.go:61-63is gone — commit658f4bamigrated the JWT validator and Bearer middleware to the externalmcp-chassisv0.1.0 module (gitea.d-ma.be/mathias/mcp-chassis/auth). The silent-degradation behaviour persists post-migration but the code lives in two repos now.What this commit covers (gitea-mcp scope):
/healthznow emits JSON with three-state JWT status:disabled(DEX_ISSUER_URL unset),enabled(validator initialized),degraded(configured but init failed — only static-token auth currently accepted).last_errorsurfaces theNewJWTValidatorerror string so ops can correlatedegradedwith Dex outage windows without tailing pod logs.cmd/gitea-mcp/healthz_test.go).Deferred to follow-up (mcp-chassis scope):
validator == nil— lives inBearerMiddleware, would log on every request that hits the chassis (mcp-chassis/auth/bearer.go:59-64).503withWWW-Authenticate: Bearer error="temporarily_unavailable"when Dex is known-down —Validatewould need to distinguish "validator not configured" from "validator unhealthy"; touches the auth contract for every MCP consumer (gitea-mcp, brain-mcp, future template-go-agent spawn).Both items require coordinated changes in chassis + a v0.1.1 tag +
go.modbumps in every consumer. Worth opening a chassis-side issue. Leaving this one open until that's done; close when chassis ships.Closed across two layers.
Already shipped in gitea-mcp (present before this pass):
/healthzreports the JWT validator state —disabled(DEX_ISSUER_URL unset) /enabled/degraded(configured but init failed) withlast_error— and startup logs a warning when the validator fails to initialize. That covers the "silent degradation is now observable" asks (newHealthzHandler,TestHealthzHandler).New in mcp-chassis v0.3.0 (adopted by gitea-mcp v0.5.0): the runtime Dex-outage case is now distinct from a bad token.
JWTValidator.Validatetags a JWKS/Dex fetch failure with the exportedErrUnavailablesentinel, andBearerMiddlewaremaps it to HTTP 503 withWWW-Authenticate: Bearer error="temporarily_unavailable"(RFC 6750 §3.1) instead of a generic 401 — so a caller can tell "Dex is down, retry" from "your token is invalid". Covered byTestValidate_JWKSUnreachable_IsUnavailable+TestBearerMiddleware_DexDown_503.Scope note: the 503 fires for a runtime JWKS-fetch failure. The startup-degraded case (validator nil because Dex was down at boot) still returns 401 at the middleware, because
BearerMiddlewarereceives a nil validator and can't distinguish "JWT disabled" from "degraded" without a signature change across all chassis consumers —/healthzsurfaces that state instead. If you want startup-degraded to also emit 503, that's a follow-up chassis signature change; noting rather than silently doing it.