fix: fetch and apply csrf to WS call (#1679)

* fix: fetch and apply csrf to WS call

* fix: update tests

* fix: update more tests

* fix: update more tests
This commit is contained in:
Tobias Åström
2025-02-24 08:53:34 +01:00
committed by GitHub
parent a860ca6d55
commit 3f87dfc2b7
13 changed files with 75 additions and 25 deletions

View File

@@ -189,4 +189,4 @@
}
}
}
}
}

View File

@@ -3,6 +3,9 @@ import * as ENIGMA from 'enigma.js';
import qixSchema from 'enigma.js/schemas/12.2015.0.json';
import * as SenseUtilities from 'enigma.js/sense-utilities';
import { connect, openApp, getConnectionInfo, getParams, parseEngineURL } from '../connect';
import * as getCsrfToken from '../utils/getCsrfToken';
jest.mock('../utils/getCsrfToken', () => jest.fn());
jest.mock('@qlik/sdk');
jest.mock('enigma.js');
@@ -45,6 +48,7 @@ describe('connect.js', () => {
generateWebsocketUrl: generateWebsocketUrlMock,
}));
windowFetchSpy = jest.spyOn(window, 'fetch');
getCsrfToken.mockResolvedValue('A-CSRF-TOKEN');
});
afterEach(() => {
@@ -119,6 +123,9 @@ describe('connect.js', () => {
enigma: {
secure: false,
host: 'localhost:1234/app/engineData',
urlParams: {
'qlik-csrf-token': 'A-CSRF-TOKEN',
},
},
};
jsonResponseMock.mockImplementation(() => Promise.resolve(connectionResponse));

View File

@@ -59,7 +59,7 @@ const ConnectionGuid = ({ showGuid }) => (
<br />
</Typography>
<Typography variant="subtitle1" gutterBottom>
Qlik Sense Enterprise on Windows
Qlik Sense on Windows
</Typography>
<Typography variant="body2" paragraph>
WebSocket URL format: <code>wss://&lt;sense-host.com&gt;/&lt;virtual-proxy-prefix&gt;</code>
@@ -68,9 +68,8 @@ const ConnectionGuid = ({ showGuid }) => (
<br />
<br />
Note that for the Qlik Sense Proxy to allow sessions from this webpage,
<code>{window.location.host}</code> needs to be whitelisted in QMC in your Qlik Sense Enterprise on Windows
deployment. In addition, you need to enable <i>Has secure attribute</i> and set <i>SameSite attribute</i> to{' '}
<i>None</i>.
<code>{window.location.host}</code> needs to be whitelisted in QMC in your Qlik Sense on Windows deployment. In
addition, you need to enable <i>Has secure attribute</i> and set <i>SameSite attribute</i> to <i>None</i>.
<br />
Make sure you are logged in to Qlik Sense in another browser tab.
</Typography>

View File

@@ -11,7 +11,7 @@ describe('<ConnectionGuid />', () => {
'Web integration id format:',
'OAuth Client ID URL format:',
'Qlik Cloud Services',
'Qlik Sense Enterprise on Windows',
'Qlik Sense on Windows',
'Qlik Sense Desktop',
].map((title) => {
expect(screen.queryByText(title)).toBeInTheDocument();

View File

@@ -24,7 +24,7 @@ describe('<SelectEngine />', () => {
'Web integration id format:',
'OAuth Client ID URL format:',
'Qlik Cloud Services',
'Qlik Sense Enterprise on Windows',
'Qlik Sense on Windows',
'Qlik Sense Desktop',
].map((title) => {
expect(screen.queryByText(title)).toBeVisible();
@@ -43,7 +43,7 @@ describe('<SelectEngine />', () => {
'Web integration id format:',
'OAuth Client ID URL format:',
'Qlik Cloud Services',
'Qlik Sense Enterprise on Windows',
'Qlik Sense on Windows',
'Qlik Sense Desktop',
].map((title) => {
expect(screen.queryByText(title)).not.toBeVisible();

View File

@@ -2,6 +2,7 @@ import enigma from 'enigma.js';
import qixSchema from 'enigma.js/schemas/12.2015.0.json';
import SenseUtilities from 'enigma.js/sense-utilities';
import { Auth, AuthType } from '@qlik/sdk';
import getCsrfToken from './utils/getCsrfToken';
const getParams = () => {
const opts = {};
@@ -167,9 +168,11 @@ const connect = async () => {
};
}
const csrfToken = await getCsrfToken(`https://${enigmaInfo.host}/${enigmaInfo.prefix}`);
const url = SenseUtilities.buildUrl({
secure: false,
...enigmaInfo,
...{ urlParams: { 'qlik-csrf-token': csrfToken } },
});
return enigma.create({ schema: qixSchema, url }).open();

View File

@@ -7,6 +7,9 @@ import {
} from '../useConnection';
import * as connectModule from '../../connect';
import { RouterWrapper } from '../../utils';
import getCsrfToken from '../../utils/getCsrfToken';
jest.mock('../../utils/getCsrfToken', () => jest.fn());
describe('useConnection Module', () => {
let connectMock;
@@ -29,6 +32,7 @@ describe('useConnection Module', () => {
connectMock = jest.fn().mockResolvedValue(glob);
jest.spyOn(connectModule, 'connect').mockImplementation(connectMock);
getCsrfToken.mockResolvedValue('A-CSRF-TOKEN');
});
afterEach(() => {
@@ -213,7 +217,9 @@ describe('useConnection Module', () => {
expect(setError).toHaveBeenCalledTimes(1);
expect(setError).toHaveBeenCalledWith({
hints: [
'If you are connecting to Qlik Cloud Services, make sure to provide a web integration id or client id.',
'- If you are connecting to Qlik Cloud, make sure to provide a web integration id or client id.',
'- For Qlik Sense on Windows, make sure the proxy setup is correct and that you are authenticated.',
'- Press the ? in the top right for more information on how to set up the connection correctly.',
],
message: 'Connection failed to wss://some.remote.sde.qlikdev.com',
});

View File

@@ -4,9 +4,11 @@ import * as SenseUtilities from 'enigma.js/sense-utilities';
import qixSchema from 'enigma.js/schemas/12.2015.0.json';
import { useOpenApp } from '../useOpenApp';
import * as getAuthInstanceModule from '../../connect';
import * as getCsrfToken from '../../utils/getCsrfToken';
jest.mock('enigma.js');
jest.mock('enigma.js/sense-utilities');
jest.mock('../../utils/getCsrfToken', () => jest.fn());
describe('useOpenApp()', () => {
let renderResult;
@@ -56,6 +58,8 @@ describe('useOpenApp()', () => {
});
jest.spyOn(getAuthInstanceModule, 'getAuthInstance').mockImplementation(getAuthInstanceMock);
windowFetchSpy = jest.spyOn(window, 'fetch');
getCsrfToken.mockResolvedValue('A-CSRF-TOKEN');
});
afterEach(() => {

View File

@@ -49,9 +49,11 @@ export const handleConnectionFailure = ({ error, info, setError }) => {
if (error.target instanceof WebSocket) {
oops.message = `Connection failed to ${info.engineUrl}`;
if (/\.qlik[A-Za-z0-9-]+\.com/.test(info.engineUrl) && !info.webIntegrationId) {
oops.hints.push('- If you are connecting to Qlik Cloud, make sure to provide a web integration id or client id.');
oops.hints.push(
'If you are connecting to Qlik Cloud Services, make sure to provide a web integration id or client id.'
'- For Qlik Sense on Windows, make sure the proxy setup is correct and that you are authenticated.'
);
oops.hints.push('- Press the ? in the top right for more information on how to set up the connection correctly.');
}
setError(oops);
return;

View File

@@ -3,6 +3,7 @@ import enigma from 'enigma.js';
import qixSchema from 'enigma.js/schemas/12.2015.0.json';
import SenseUtilities from 'enigma.js/sense-utilities';
import { getAuthInstance } from '../connect';
import getCsrfToken from '../utils/getCsrfToken';
export const useOpenApp = ({ info }) => {
const [app, setApp] = useState(null);
@@ -29,7 +30,8 @@ export const useOpenApp = ({ info }) => {
const { webSocketUrl } = await (await fetch(`/auth/getSocketUrl/${info?.enigma.appId}`)).json();
url = webSocketUrl;
} else {
url = SenseUtilities.buildUrl(enigmaInfo);
const csrfToken = await getCsrfToken(`https://${enigmaInfo.host}/${enigmaInfo.prefix}`);
url = SenseUtilities.buildUrl({ ...enigmaInfo, ...{ urlParams: { 'qlik-csrf-token': csrfToken } } });
}
const enigmaGlobal = await enigma.create({ schema: qixSchema, url }).open();

View File

@@ -1,4 +1,7 @@
import { getAppLink } from '../appLinkManager';
import * as getCsrfToken from '../getCsrfToken';
jest.mock('../getCsrfToken', () => jest.fn());
describe('getAppLink()', () => {
let navigate;
@@ -11,6 +14,7 @@ describe('getAppLink()', () => {
navigate = jest.fn();
location = { search: '' };
targetApp = 'targetAppId';
getCsrfToken.mockResolvedValue('A-CSRF-TOKEN');
});
afterEach(() => {
@@ -18,58 +22,66 @@ describe('getAppLink()', () => {
jest.resetAllMocks();
});
test('should call navigate to correct engine url from localhost', () => {
test('should call navigate to correct engine url from localhost', async () => {
info = { engineUrl: 'ws://localhost:1234', enigma: { secure: false, host: 'localhost', port: 1234 } };
location.search = `engine_url=${info.engineUrl}`;
getAppLink({ navigate, location, info, targetApp });
await getAppLink({ navigate, location, info, targetApp });
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`);
expect(navigate).toHaveBeenCalledWith(
`/dev/engine_url=${info.engineUrl}/app/${targetApp}&qlik-csrf-token=A-CSRF-TOKEN`
);
});
test('should call navigate to correct engine url from localhost without prefix', () => {
test('should call navigate to correct engine url from localhost without prefix', async () => {
info = {
engineUrl: 'ws://localhost:1234',
enigma: { secure: false, host: 'localhost', port: 1234, prefix: undefined },
};
getCsrfToken.mockResolvedValue(null);
location.search = `engine_url=${info.engineUrl}`;
getAppLink({ navigate, location, info, targetApp });
await getAppLink({ navigate, location, info, targetApp });
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`);
});
test('should call navigate to correct engine url with prefix', () => {
test('should call navigate to correct engine url with prefix', async () => {
info = {
engineUrl: 'ws://localhost:1234/prefix',
enigma: { secure: false, host: 'localhost', port: 1234, prefix: 'prefix' },
};
location.search = `engine_url=${info.engineUrl}`;
getAppLink({ navigate, location, info, targetApp });
await getAppLink({ navigate, location, info, targetApp });
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`);
expect(navigate).toHaveBeenCalledWith(
`/dev/engine_url=${info.engineUrl}/app/${targetApp}&qlik-csrf-token=A-CSRF-TOKEN`
);
});
test('should call navigate to correct engine url from remote SDE', () => {
test('should call navigate to correct engine url from remote SDE', async () => {
info = {
engineUrl: 'wss://some-remote.sde.in.eu.qlikdev.com',
enigma: { secure: true, host: 'some-remote.sde.in.eu.qlikdev.com' },
};
getCsrfToken.mockResolvedValue(null);
location.search = `engine_url=${info.engineUrl}`;
getAppLink({ navigate, location, info, targetApp });
await getAppLink({ navigate, location, info, targetApp });
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`);
});
test('should remove `shouldFetchAppList` if it was in search', () => {
test('should remove `shouldFetchAppList` if it was in search', async () => {
info = {
engineUrl: 'wss://some-remote.sde.in.eu.qlikdev.com',
enigma: { secure: true, host: 'some-remote.sde.in.eu.qlikdev.com' },
};
getCsrfToken.mockResolvedValue(null);
location.search = `engine_url=${info.engineUrl}&shouldFetchAppList=true`;
getAppLink({ navigate, location, info, targetApp });
await getAppLink({ navigate, location, info, targetApp });
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`);

View File

@@ -1,10 +1,16 @@
export const getAppLink = ({ navigate, location, targetApp, info }) => {
import getCsrfToken from './getCsrfToken';
export const getAppLink = async ({ navigate, location, targetApp, info }) => {
const { search } = location;
const protocol = info.enigma.secure ? 'wss' : 'ws';
const port = info.enigma.port ? `:${info.enigma.port}` : '';
const prefix = info.enigma.prefix ? `/${info.enigma.prefix}` : '';
const host = (info.enigma.host === 'localhost' ? `${info.enigma.host}${port}` : info.enigma.host) + prefix;
const newEngineUrl = `${protocol}://${host}/app/${encodeURIComponent(targetApp)}`;
const csrfToken = await getCsrfToken(`https://${info.enigma.host}${prefix}`);
const csrfQuery = csrfToken ? `&qlik-csrf-token=${csrfToken}` : '';
const newEngineUrl = `${protocol}://${host}/app/${encodeURIComponent(targetApp)}${csrfQuery}`;
const modifiedEngineUrl = search.replace(info.engineUrl, newEngineUrl);
navigate(`/dev/${modifiedEngineUrl}`.replace('&shouldFetchAppList=true', ''));

View File

@@ -0,0 +1,9 @@
export default async function getCsrfToken(host) {
try {
const res = await fetch(`${host}/qps/csrftoken`, { credentials: 'include' });
return res.headers.get('QLIK-CSRF-TOKEN');
} catch (err) {
console.log('Failed to fetch csrf-token', err);
}
return '';
}