Skip to content

fix(auth): meaningful error when ClickHouse refuses an authenticated user#11

Merged
BorisTyshkevich merged 1 commit into
mainfrom
fix/clickhouse-authz-message
Jun 21, 2026
Merged

fix(auth): meaningful error when ClickHouse refuses an authenticated user#11
BorisTyshkevich merged 1 commit into
mainfrom
fix/clickhouse-authz-message

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

Problem

When a user signs in via the IdP successfully but ClickHouse won't authorize them — no matching CH user, missing grants, or a JWT it can't verify — the app showed "Session expired" and bounced to the login screen. The session wasn't expired, and re-authenticating never helped. The user just saw repeated 403s with an unhelpful message.

Root cause

authedFetch (src/net/ch-client.js) flagged any 401/403 as "auth expired." But by the time it POSTs, getToken() has already guaranteed a valid, non-expired bearer (it refreshes, or returns null and the caller signs out). So a 401/403 that survives the single refresh-retry means ClickHouse rejected a valid login — an authorization/identity problem, not expiry. The code nonetheless called onSignedOut()renderLogin(app, 'Session expired').

Fix

Distinguish the two failure modes:

  • CH rejects a valid login → show an accurate message naming the cause and appending ClickHouse's own reason, e.g.:

    ClickHouse denied your account (HTTP 403). You're signed in, but this server is not authorizing you — your identity may have no ClickHouse user or the required grants. Server: Code: 516. DB::Exception: Invalid authentication: Token could not be verified. (AUTHENTICATION_FAILED)

  • Genuine expiry (no token, or expired + refresh failed) keeps its own path, reworded to "Your session expired — please sign in again."

Changes:

  • core/stream.js — new pure authDeniedMessage(status, reason) (reuses parseExceptionText for the server reason)
  • net/ch-client.js — classify a valid-token 401/403 as authorization denial, pass CH's reason to onSignedOut
  • ui/app.jsonSignedOut(detail) renders the detail, else the expiry default

Tests

npm test green; per-file gate held — stream.js and ch-client.js at 100%. Added authDeniedMessage cases, updated the ch-client 403 test to assert the message + server reason, and added an app.js test for both onSignedOut message paths.

Verification

Built and deployed to antalya + otel. Reproduced live on antalya by injecting a non-expired but unverifiable JWT (so getToken() returns it and ClickHouse 403s it): the login screen shows the authorization message + the real AUTHENTICATION_FAILED reason instead of "Session expired" (screenshot below).

🤖 Generated with Claude Code

https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef

…icated user

A valid IdP login that ClickHouse won't authorize (no CH user, missing grants,
or an unverifiable JWT) surfaced as "Session expired" on the login screen, and
re-authenticating never helped.

Root cause: authedFetch flagged any 401/403 as auth-expired. But getToken()
already guarantees a non-expired bearer before the POST, so a 401/403 that
survives the single refresh-retry means CH rejected a *valid* login — an
authorization/identity problem, not session expiry.

Now that case shows an accurate message that names the cause and appends
ClickHouse's own reason (e.g. "Code: 516 ... Token could not be verified").
Genuine expiry (no token, or expired + refresh failed) keeps its own path,
reworded to "Your session expired — please sign in again."

- core/stream.js: new pure authDeniedMessage(status, reason)
- net/ch-client.js: classify valid-token 401/403 as authorization denial,
  passing CH's reason to onSignedOut
- ui/app.js: onSignedOut(detail) renders the detail, else the expiry default

Verified live on antalya by injecting a non-expired but unverifiable token:
the login screen shows the authorization message + AUTHENTICATION_FAILED reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
@BorisTyshkevich BorisTyshkevich merged commit 2ae92d8 into main Jun 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant