mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
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:
@@ -189,4 +189,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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://<sense-host.com>/<virtual-proxy-prefix></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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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', ''));
|
||||
|
||||
9
commands/serve/web/utils/getCsrfToken.js
Normal file
9
commands/serve/web/utils/getCsrfToken.js
Normal 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 '';
|
||||
}
|
||||
Reference in New Issue
Block a user