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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ auto-generated per-PR notes; this file is the curated, human-readable history.

## [Unreleased]

### Fixed
- Remove obsolete `iss` and `hd` login-link hints from SQL Browser URLs. IdP
selection remains owned by the deployment configuration and session state.

## [0.6.3] - 2026-07-24

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions src/core/sql-route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Pure parser/builder for the single `/sql` application route (#407).
// The caller supplies `location.search`; this module owns only `ws`, `surface`,
// and `mode`, leaving OAuth callback and other application parameters intact.
// `iss`/`hd` are retired legacy login-link hints: IdP selection comes from
// config.json/session state, never from URL-supplied issuer/domain values.

export type SqlRoute =
| { surface: 'workspace'; workspaceKey: string | null }
Expand Down Expand Up @@ -32,6 +34,8 @@ export function buildSqlRouteSearch(route: SqlRoute, currentSearch = ''): string
// Retired Dashboard snapshot route state (#407); never carry it forward.
params.delete('st');
params.delete('dash');
params.delete('iss');
params.delete('hd');
if (route.workspaceKey !== null) params.set('ws', route.workspaceKey);
if (route.surface === 'dashboard') {
params.set('surface', 'dashboard');
Expand Down
26 changes: 23 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { handleKeydown } from './ui/shortcuts.js';
import { exchangeCodeForTokens, bearerFromTokens } from './net/oauth.js';
import { decodeShare } from './core/share.js';
import { cloneJson, queryName, queryPanel, queryView, upgradeSavedQuery } from './core/saved-query.js';
import { parseSqlRoute } from './core/sql-route.js';
import { normalizeSqlRouteSearch, parseSqlRoute } from './core/sql-route.js';
import { rolePreviewView } from './core/result-choice.js';
import { isQuerylessPanel } from './core/panel-cfg.js';
import { setTabSpecDraft, SAVED_VIEWS } from './state.js';
Expand Down Expand Up @@ -63,8 +63,21 @@ export async function bootstrap(app: BootstrapApp, env: BootstrapEnv): Promise<{
const loc = env.location;
const ss = env.sessionStorage;
const hist = env.history;
let dash = parseSqlRoute(loc.search).surface === 'dashboard';
// Canonicalize retired route parameters before showing either the login or
// workbench surface. `iss`/`hd` must not look like active IdP selectors:
// actual selection is config/session-owned.
const initialParams = new URLSearchParams(loc.search);
const hasRetiredLoginHint = initialParams.has('iss') || initialParams.has('hd');
const normalizedRoute = hasRetiredLoginHint
? normalizeSqlRouteSearch(loc.search)
: { route: parseSqlRoute(loc.search), search: loc.search };
if (normalizedRoute.search !== loc.search) {
hist.replaceState(null, '', loc.origin + loc.pathname + normalizedRoute.search + loc.hash);
app.syncSqlRoute(normalizedRoute.search);
}
let dash = normalizedRoute.route.surface === 'dashboard';
const u = new URL(loc.href);
u.search = normalizedRoute.search;
const code = u.searchParams.get('code');
const stateParam = u.searchParams.get('state');
const expectedState = ss.getItem('oauth_state');
Expand Down Expand Up @@ -118,7 +131,14 @@ export async function bootstrap(app: BootstrapApp, env: BootstrapEnv): Promise<{
}
}
const qs = u.searchParams.toString();
const cleanedSearch = qs ? '?' + qs : '';
const callbackSearch = qs ? '?' + qs : '';
// A callback can restore return-route state saved by an older version.
// Canonicalize retired hints again so they cannot reappear on a failed
// sign-in, which renders the login screen without a workspace rewrite.
const callbackParams = new URLSearchParams(callbackSearch);
const cleanedSearch = callbackParams.has('iss') || callbackParams.has('hd')
? normalizeSqlRouteSearch(callbackSearch).search
: callbackSearch;
hist.replaceState(null, '', loc.origin + loc.pathname + cleanedSearch + loc.hash);
app.syncSqlRoute(cleanedSearch);
dash = parseSqlRoute(cleanedSearch).surface === 'dashboard';
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,4 +478,38 @@ describe('bootstrap', () => {
expect(url).toContain('keep=1');
expect(url).not.toContain('code=');
});

it('removes retired issuer and hosted-domain hints before rendering the login screen', async () => {
const app = fakeApp();
const env = fakeEnv({
location: asLocation({
href: 'https://ch/sql?iss=https%3A%2F%2Faccounts.google.com&hd=altinity.com&ws=ops',
origin: 'https://ch', pathname: '/sql',
search: '?iss=https%3A%2F%2Faccounts.google.com&hd=altinity.com&ws=ops', hash: '',
}),
});
await bootstrap(app, env);
expect(env.history.replaceState).toHaveBeenCalledWith(null, '', 'https://ch/sql?ws=ops');
expect(app.syncSqlRoute).toHaveBeenCalledWith('?ws=ops');
expect(app.showLogin).toHaveBeenCalled();
});

it('removes retired login hints restored by an OAuth error callback', async () => {
const app = fakeApp();
const env = fakeEnv({
location: asLocation({
href: 'https://ch/sql?error=access_denied&state=st', origin: 'https://ch',
pathname: '/sql', search: '?error=access_denied&state=st', hash: '',
}),
});
env.sessionStorage.setItem('oauth_state', 'st');
env.sessionStorage.setItem('oauth_return_route', JSON.stringify({
state: 'st', search: '?iss=https%3A%2F%2Faccounts.google.com&hd=altinity.com&ws=ops',
}));
await bootstrap(app, env);
expect(env.history.replaceState).toHaveBeenCalledWith(null, '', 'https://ch/sql?ws=ops');
expect(app.syncSqlRoute).toHaveBeenCalledWith('?ws=ops');
expect(env.sessionStorage.getItem('oauth_return_route')).toBeNull();
expect(app.showLogin).toHaveBeenCalledWith('Sign-in failed: access_denied');
});
});
5 changes: 5 additions & 0 deletions tests/unit/sql-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ describe('buildSqlRouteSearch', () => {
expect(normalizeSqlRouteSearch('?ws=x&st=token&dash=old&keep=yes').search)
.toBe('?keep=yes&ws=x');
});

it('drops the retired issuer and hosted-domain login-link hints', () => {
expect(normalizeSqlRouteSearch('?iss=https%3A%2F%2Faccounts.google.com&hd=altinity.com&ws=x').search)
.toBe('?ws=x');
});
});

describe('routeForWorkspace', () => {
Expand Down
Loading