Misleading auth log: static_token_mismatch reported for token_type: "jwt" when the JWT validator is unavailable #58

Closed
opened 2026-07-17 13:02:46 +00:00 by mathias · 1 comment
Owner

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.
## 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.
mathias added the risk/mediumsize/stype/fix labels 2026-08-27 05:25:40 +00:00
Author
Owner

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

## 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_
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/gitea-mcp#58