fix: prevent duplicate preview URL auth tokens
Stops embedded browser URLs from accumulating repeated oc_url_token params Strips preview-only query params before persisting browser navigation Adds regression coverage for replacing URL auth tokens
This commit is contained in:
@@ -447,6 +447,21 @@ const stripPreviewTokenFromUrl = (value: string): string => {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const stripPreviewQueryParams = (value: string): string => {
|
||||
if (!value) return value;
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
parsed.searchParams.delete('ocPreview');
|
||||
parsed.searchParams.delete('oc_preview_token');
|
||||
parsed.searchParams.delete('oc_client_token');
|
||||
parsed.searchParams.delete('oc_url_token');
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const PreviewPane: React.FC<PreviewPaneProps> = ({ rawUrl, onNavigate }) => {
|
||||
const { t } = useI18n();
|
||||
const { currentTheme } = useThemeSystem();
|
||||
@@ -593,6 +608,8 @@ const PreviewPane: React.FC<PreviewPaneProps> = ({ rawUrl, onNavigate }) => {
|
||||
? (() => {
|
||||
const path = normalizedUrl.pathname || '/';
|
||||
const searchParams = new URLSearchParams(normalizedUrl.search);
|
||||
searchParams.delete('oc_url_token');
|
||||
searchParams.delete('oc_client_token');
|
||||
searchParams.set('ocPreview', String(reloadNonce));
|
||||
searchParams.set('oc_preview_token', proxyState.previewToken || '');
|
||||
const search = searchParams.toString();
|
||||
@@ -1394,6 +1411,8 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
const parsed = new URL(currentUrl);
|
||||
const path = parsed.pathname || '/';
|
||||
const searchParams = new URLSearchParams(parsed.search);
|
||||
searchParams.delete('oc_url_token');
|
||||
searchParams.delete('oc_client_token');
|
||||
searchParams.set('ocPreview', String(reloadNonce));
|
||||
searchParams.set('oc_preview_token', proxyState.previewToken || '');
|
||||
const search = searchParams.toString();
|
||||
@@ -1418,7 +1437,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
|
||||
const rest = parsedFrameUrl.pathname.slice(proxyBasePath.length) || '/';
|
||||
const upstreamOrigin = new URL(currentUrl).origin;
|
||||
return new URL(`${rest}${parsedFrameUrl.search}${parsedFrameUrl.hash}`, upstreamOrigin).toString();
|
||||
return stripPreviewQueryParams(new URL(`${rest}${parsedFrameUrl.search}${parsedFrameUrl.hash}`, upstreamOrigin).toString());
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@@ -1440,7 +1459,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
return '';
|
||||
}
|
||||
|
||||
return new URL(`${parsedFrameUrl.pathname}${parsedFrameUrl.search}${parsedFrameUrl.hash}`, upstreamOrigin).toString();
|
||||
return stripPreviewQueryParams(new URL(`${parsedFrameUrl.pathname}${parsedFrameUrl.search}${parsedFrameUrl.hash}`, upstreamOrigin).toString());
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -120,6 +120,19 @@ describe('createRuntimeUrlResolver', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('replaces existing short-lived URL auth query on relative authenticated URLs', () => {
|
||||
setRuntimeUrlAuthToken('oc_url_secret', Date.now() + 60_000);
|
||||
try {
|
||||
const urls = createRuntimeUrlResolver();
|
||||
|
||||
expect(urls.authenticatedAsset('/api/preview/proxy/abc/?oc_url_token=stale&x=1#top')).toBe(
|
||||
'/api/preview/proxy/abc/?oc_url_token=oc_url_secret&x=1#top',
|
||||
);
|
||||
} finally {
|
||||
setRuntimeUrlAuthToken(null, null);
|
||||
}
|
||||
});
|
||||
|
||||
test('does not put the long-lived client token in URLs', () => {
|
||||
setRuntimeBearerToken('oc_client_secret');
|
||||
try {
|
||||
|
||||
@@ -91,14 +91,12 @@ const withUrlAuth = (urlValue: string): string => {
|
||||
const token = getRuntimeUrlAuthTokenSync();
|
||||
if (!token) return urlValue;
|
||||
|
||||
if (ABSOLUTE_URL_PATTERN.test(urlValue)) {
|
||||
const url = new URL(urlValue);
|
||||
url.searchParams.set('oc_url_token', token);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const separator = urlValue.includes('?') ? '&' : '?';
|
||||
return `${urlValue}${separator}oc_url_token=${encodeURIComponent(token)}`;
|
||||
const url = ABSOLUTE_URL_PATTERN.test(urlValue)
|
||||
? new URL(urlValue)
|
||||
: new URL(urlValue, 'http://openchamber.local');
|
||||
url.searchParams.set('oc_url_token', token);
|
||||
if (ABSOLUTE_URL_PATTERN.test(urlValue)) return url.toString();
|
||||
return `${url.pathname}${url.search}${url.hash}`;
|
||||
};
|
||||
|
||||
const toWebSocketUrl = (candidate: string, config: RuntimeUrlConfig): string => {
|
||||
|
||||
Reference in New Issue
Block a user