The reason is technically accurate — the code did fall through to a static-token comparison and it did mismatch — but it is actively misleading. static_token_mismatch reads as "the caller presented the wrong token". The truth was "the server's JWT validator is dead, so a correct token had no chance of validating". The fields even contradict each other: token_type: "jwt" next to a static token failure is the tell, and it is easy to miss.
This misdirection propagated all the way to the user: claude.ai surfaced Authorization with gitea failed. You can check your credentials and permissions. The credentials were valid. Time went into re-running the Authentik OAuth flow before the server logs were read.
Proposed fix
When a JWT is presented and the validator is unavailable, log a distinct reason:
Reserve static_token_mismatch for token_type: "static". Never let a JWT path report a static-token failure.
Consider also returning 503 rather than 401 in this case: the server is degraded, the request is not unauthorized. A 503 would tell an upstream client to retry rather than tell a user to check their credentials.
Acceptance criterion
A test asserting that with a nil/failed validator, a well-formed JWT yields reason jwt_validator_unavailable (not static_token_mismatch).
Refs
Companion to the one-shot validator init issue.
Observed on v0.11.0, koala k3s, 2026-07-17.
## Problem
When the JWT validator failed to initialize, every rejected JWT logged this:
```
WARN mcp auth rejected reason="static_token_mismatch" status=401 token_type="jwt" token_fp="872d2da957ac" path="/mcp"
```
The reason is technically accurate — the code did fall through to a static-token comparison and it did mismatch — but it is actively misleading. `static_token_mismatch` reads as "the caller presented the wrong token". The truth was "the server's JWT validator is dead, so a correct token had no chance of validating". The fields even contradict each other: `token_type: "jwt"` next to a *static* token failure is the tell, and it is easy to miss.
This misdirection propagated all the way to the user: claude.ai surfaced `Authorization with gitea failed. You can check your credentials and permissions.` The credentials were valid. Time went into re-running the Authentik OAuth flow before the server logs were read.
## Proposed fix
When a JWT is presented and the validator is unavailable, log a distinct reason:
```
WARN mcp auth rejected reason="jwt_validator_unavailable" status=401 token_type="jwt" ...
```
Reserve `static_token_mismatch` for `token_type: "static"`. Never let a JWT path report a static-token failure.
Consider also returning 503 rather than 401 in this case: the server is degraded, the request is not unauthorized. A 503 would tell an upstream client to retry rather than tell a user to check their credentials.
## Acceptance criterion
A test asserting that with a nil/failed validator, a well-formed JWT yields reason `jwt_validator_unavailable` (not `static_token_mismatch`).
## Refs
- Companion to the one-shot validator init issue.
- Observed on v0.11.0, koala k3s, 2026-07-17.
Fixed in mcp-chassis v0.7.0, consumed here in v0.15.0.
The fix is the one this issue asked for
Reserve static_token_mismatch for token_type: "static". Never let a JWT path report a static-token failure.
The fall-through reason was chosen by which comparison ran last, not by what the caller presented. It now branches on the presented credential:
presented
validator
reason
JWT
nil (no issuer configured)
jwt_auth_not_configured
opaque
nil
static_token_mismatch (unchanged)
JWT
non-nil, issuer unreachable
jwt_dex_unavailable, 503
JWT
non-nil, token bad
jwt_invalid, 401
Acceptance criterion
A test asserting that with a nil/failed validator, a well-formed JWT yields reason jwt_validator_unavailable (not static_token_mismatch).
auth/bearer_logging_test.go asserts it, with one deliberate difference: the reason is jwt_auth_not_configured, not jwt_validator_unavailable, because after mcp-chassis#4 those became genuinely different conditions and deserve different words.
validator exists but cannot reach its issuer → jwt_dex_unavailable, 503. This is the case that caused the incident, and it is now unreachable via the nil path at all.
validator is nil → the server was never given an issuer. Not a degradation, a configuration. jwt_auth_not_configured says that; jwt_validator_unavailable would imply something is broken when nothing is.
The companion assertion matters as much: an opaque wrong token still reports static_token_mismatch. Fixing the misleading case must not delete the accurate one.
Your second suggestion was already right, and is now the default
Consider also returning 503 rather than 401 in this case: the server is degraded, the request is not unauthorized. A 503 would tell an upstream client to retry rather than tell a user to check their credentials.
That is what v0.6.0 does. The validator is now built even when its issuer is unreachable, so the exact scenario in this issue — Authentik down at boot, valid JWT presented — returns 503 jwt_dex_unavailable with WWW-Authenticate: error="temporarily_unavailable", and self-heals without a restart. The 401 path this issue was filed against no longer exists for that condition.
Why it took this long
Filed 2026-07-17. The misleading log did its work again on 2026-09-14: the claude.ai UI said "Authorization with gitea failed. You can check your credentials and permissions", and the first move was to re-run the OAuth flow. The credentials were valid throughout, exactly as this issue predicted, and the answer was in the server log the whole time.
## Fixed in `mcp-chassis v0.7.0`, consumed here in `v0.15.0`.
### The fix is the one this issue asked for
> Reserve `static_token_mismatch` for `token_type: "static"`. Never let a JWT path report a static-token failure.
The fall-through reason was chosen by *which comparison ran last*, not by what the caller presented. It now branches on the presented credential:
| presented | validator | reason |
|---|---|---|
| JWT | nil (no issuer configured) | **`jwt_auth_not_configured`** |
| opaque | nil | `static_token_mismatch` (unchanged) |
| JWT | non-nil, issuer unreachable | `jwt_dex_unavailable`, **503** |
| JWT | non-nil, token bad | `jwt_invalid`, 401 |
### Acceptance criterion
> A test asserting that with a nil/failed validator, a well-formed JWT yields reason `jwt_validator_unavailable` (not `static_token_mismatch`).
`auth/bearer_logging_test.go` asserts it, with one deliberate difference: the reason is **`jwt_auth_not_configured`**, not `jwt_validator_unavailable`, because after mcp-chassis#4 those became genuinely different conditions and deserve different words.
- *validator exists but cannot reach its issuer* → `jwt_dex_unavailable`, **503**. This is the case that caused the incident, and it is now unreachable via the nil path at all.
- *validator is nil* → the server was never given an issuer. Not a degradation, a configuration. `jwt_auth_not_configured` says that; `jwt_validator_unavailable` would imply something is broken when nothing is.
The companion assertion matters as much: an opaque wrong token **still** reports `static_token_mismatch`. Fixing the misleading case must not delete the accurate one.
### Your second suggestion was already right, and is now the default
> Consider also returning 503 rather than 401 in this case: the server is degraded, the request is not unauthorized. A 503 would tell an upstream client to retry rather than tell a user to check their credentials.
That is what v0.6.0 does. The validator is now built even when its issuer is unreachable, so the exact scenario in this issue — Authentik down at boot, valid JWT presented — returns **503 `jwt_dex_unavailable`** with `WWW-Authenticate: error="temporarily_unavailable"`, and self-heals without a restart. The 401 path this issue was filed against no longer exists for that condition.
### Why it took this long
Filed 2026-07-17. The misleading log did its work again on **2026-09-14**: the claude.ai UI said *"Authorization with gitea failed. You can check your credentials and permissions"*, and the first move was to re-run the OAuth flow. The credentials were valid throughout, exactly as this issue predicted, and the answer was in the server log the whole time.
Closing.
---
_Created via git-mcp on behalf of @mathias_
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.
Problem
When the JWT validator failed to initialize, every rejected JWT logged this:
The reason is technically accurate — the code did fall through to a static-token comparison and it did mismatch — but it is actively misleading.
static_token_mismatchreads as "the caller presented the wrong token". The truth was "the server's JWT validator is dead, so a correct token had no chance of validating". The fields even contradict each other:token_type: "jwt"next to a static token failure is the tell, and it is easy to miss.This misdirection propagated all the way to the user: claude.ai surfaced
Authorization with gitea failed. You can check your credentials and permissions.The credentials were valid. Time went into re-running the Authentik OAuth flow before the server logs were read.Proposed fix
When a JWT is presented and the validator is unavailable, log a distinct reason:
Reserve
static_token_mismatchfortoken_type: "static". Never let a JWT path report a static-token failure.Consider also returning 503 rather than 401 in this case: the server is degraded, the request is not unauthorized. A 503 would tell an upstream client to retry rather than tell a user to check their credentials.
Acceptance criterion
A test asserting that with a nil/failed validator, a well-formed JWT yields reason
jwt_validator_unavailable(notstatic_token_mismatch).Refs
Fixed in
mcp-chassis v0.7.0, consumed here inv0.15.0.The fix is the one this issue asked for
The fall-through reason was chosen by which comparison ran last, not by what the caller presented. It now branches on the presented credential:
jwt_auth_not_configuredstatic_token_mismatch(unchanged)jwt_dex_unavailable, 503jwt_invalid, 401Acceptance criterion
auth/bearer_logging_test.goasserts it, with one deliberate difference: the reason isjwt_auth_not_configured, notjwt_validator_unavailable, because after mcp-chassis#4 those became genuinely different conditions and deserve different words.jwt_dex_unavailable, 503. This is the case that caused the incident, and it is now unreachable via the nil path at all.jwt_auth_not_configuredsays that;jwt_validator_unavailablewould imply something is broken when nothing is.The companion assertion matters as much: an opaque wrong token still reports
static_token_mismatch. Fixing the misleading case must not delete the accurate one.Your second suggestion was already right, and is now the default
That is what v0.6.0 does. The validator is now built even when its issuer is unreachable, so the exact scenario in this issue — Authentik down at boot, valid JWT presented — returns 503
jwt_dex_unavailablewithWWW-Authenticate: error="temporarily_unavailable", and self-heals without a restart. The 401 path this issue was filed against no longer exists for that condition.Why it took this long
Filed 2026-07-17. The misleading log did its work again on 2026-09-14: the claude.ai UI said "Authorization with gitea failed. You can check your credentials and permissions", and the first move was to re-run the OAuth flow. The credentials were valid throughout, exactly as this issue predicted, and the answer was in the server log the whole time.
Closing.
Created via git-mcp on behalf of @mathias