Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/net/ch-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,28 @@ export async function authedFetch(ctx, url, sql, signal) {
headers: { Authorization: authHeader(bearer) },
signal,
});
// A 2xx confirms the credentials are good for the rest of the session.
if (resp.ok) ctx.authConfirmed = true;
let authExpired = resp.status === 401 || resp.status === 403;
if (!authExpired && !resp.ok) {
const peek = await resp.clone().text();
if (isAuthExpiredBody(peek)) authExpired = true;
}
if (authExpired) {
// Once this session has authenticated successfully, the same credentials
// are still valid — so a later 401/403 is a *query-level* error ClickHouse
// maps to that HTTP status (ACCESS_DENIED, or UNKNOWN_USER from e.g.
// `SHOW CREATE USER <missing>`), not a sign-in problem. Return it so the
// caller shows it as a normal query error instead of force-logging-out.
if (ctx.authConfirmed) return resp;
if (attempt === 0 && (await ctx.refresh())) {
bearer = await ctx.getToken();
attempt++;
continue;
}
// getToken() already guaranteed a non-expired token above, so a 401/403
// that survives the one refresh-retry means CH rejected a *valid* login —
// an authorization/identity problem, not session expiry. Surface CH's own
// reason so it's diagnosable.
// First-contact 401/403 with a non-expired token: CH rejected the login
// itself — an authorization/identity problem, not session expiry. Surface
// CH's own reason so it's diagnosable.
const reason = parseExceptionText(await resp.clone().text());
ctx.onSignedOut(authDeniedMessage(resp.status, reason));
throw new Error('signed out');
Expand Down
4 changes: 4 additions & 0 deletions src/ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function createApp(env = {}) {
app.idpId = null;
app.authMode = 'oauth';
chCtx.origin = loc.origin;
chCtx.authConfirmed = false; // a fresh sign-in starts unconfirmed again
['oauth_id_token', 'oauth_refresh_token', 'oauth_verifier', 'oauth_state', 'oauth_idp',
'ch_basic_auth', 'ch_basic_user', 'ch_basic_origin'].forEach((k) => ss.removeItem(k));
}
Expand Down Expand Up @@ -184,6 +185,9 @@ export function createApp(env = {}) {
// Where queries POST: the serving origin for OAuth, or the (possibly
// cross-origin) target chosen at credential sign-in for basic mode.
origin: app.authMode === 'basic' ? (ss.getItem('ch_basic_origin') || loc.origin) : loc.origin,
// Flips true after the first 2xx; gates whether a later 401/403 is treated
// as a sign-in failure (only before auth is confirmed) or a query error.
authConfirmed: false,
getToken,
refresh,
authHeader,
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/ch-client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ describe('authedFetch', () => {
expect(msg).toContain('not authorizing you');
expect(msg).toContain('Server: Code: 516. DB::Exception: Authentication failed');
});
it('marks the ctx authenticated on a successful response', async () => {
const ctx = ctxWith(async () => jsonResp({ ok: 1 }));
await authedFetch(ctx, 'u', 'sql');
expect(ctx.authConfirmed).toBe(true);
});
it('once authenticated, a later 403 is returned as a query error (no sign-out)', async () => {
// e.g. SHOW CREATE USER <missing> → HTTP 403 / UNKNOWN_USER, mid-session.
const ctx = ctxWith(async () => textResp('Code: 192. DB::Exception: There is no user x', false, 403),
{ authConfirmed: true });
const resp = await authedFetch(ctx, 'u', 'sql');
expect(resp.status).toBe(403);
expect(ctx.onSignedOut).not.toHaveBeenCalled();
expect(ctx.refresh).not.toHaveBeenCalled();
});
it('treats a token_verification body as auth-expired', async () => {
let n = 0;
const ctx = ctxWith(
Expand Down
Loading