diff --git a/apis/stardust/api-spec/listbox-spec.json b/apis/stardust/api-spec/listbox-spec.json
index 6b1ffd595..54a8264f5 100644
--- a/apis/stardust/api-spec/listbox-spec.json
+++ b/apis/stardust/api-spec/listbox-spec.json
@@ -189,4 +189,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/commands/serve/web/__tests__/connect.test.js b/commands/serve/web/__tests__/connect.test.js
index 2c9f593d3..5701a4a59 100644
--- a/commands/serve/web/__tests__/connect.test.js
+++ b/commands/serve/web/__tests__/connect.test.js
@@ -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));
diff --git a/commands/serve/web/components/Hub/SelectEngine/ConnectionGuid.jsx b/commands/serve/web/components/Hub/SelectEngine/ConnectionGuid.jsx
index bbbd1161a..b113bcab0 100644
--- a/commands/serve/web/components/Hub/SelectEngine/ConnectionGuid.jsx
+++ b/commands/serve/web/components/Hub/SelectEngine/ConnectionGuid.jsx
@@ -59,7 +59,7 @@ const ConnectionGuid = ({ showGuid }) => (
- Qlik Sense Enterprise on Windows
+ Qlik Sense on Windows
WebSocket URL format: wss://<sense-host.com>/<virtual-proxy-prefix>
@@ -68,9 +68,8 @@ const ConnectionGuid = ({ showGuid }) => (
Note that for the Qlik Sense Proxy to allow sessions from this webpage,
- {window.location.host} needs to be whitelisted in QMC in your Qlik Sense Enterprise on Windows
- deployment. In addition, you need to enable Has secure attribute and set SameSite attribute to{' '}
- None.
+ {window.location.host} needs to be whitelisted in QMC in your Qlik Sense on Windows deployment. In
+ addition, you need to enable Has secure attribute and set SameSite attribute to None.
Make sure you are logged in to Qlik Sense in another browser tab.
diff --git a/commands/serve/web/components/Hub/SelectEngine/__tests__/ConnectionGuid.test.jsx b/commands/serve/web/components/Hub/SelectEngine/__tests__/ConnectionGuid.test.jsx
index 46f231c78..1010a5207 100644
--- a/commands/serve/web/components/Hub/SelectEngine/__tests__/ConnectionGuid.test.jsx
+++ b/commands/serve/web/components/Hub/SelectEngine/__tests__/ConnectionGuid.test.jsx
@@ -11,7 +11,7 @@ describe('', () => {
'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();
diff --git a/commands/serve/web/components/Hub/SelectEngine/__tests__/SelectEngine.test.jsx b/commands/serve/web/components/Hub/SelectEngine/__tests__/SelectEngine.test.jsx
index d42d8c144..0d1e7d05c 100644
--- a/commands/serve/web/components/Hub/SelectEngine/__tests__/SelectEngine.test.jsx
+++ b/commands/serve/web/components/Hub/SelectEngine/__tests__/SelectEngine.test.jsx
@@ -24,7 +24,7 @@ describe('', () => {
'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('', () => {
'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();
diff --git a/commands/serve/web/connect.js b/commands/serve/web/connect.js
index 20e042e97..9a0ea6ea2 100644
--- a/commands/serve/web/connect.js
+++ b/commands/serve/web/connect.js
@@ -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();
diff --git a/commands/serve/web/hooks/__tests__/useConnection.test.js b/commands/serve/web/hooks/__tests__/useConnection.test.js
index 682785c1d..56f65da52 100644
--- a/commands/serve/web/hooks/__tests__/useConnection.test.js
+++ b/commands/serve/web/hooks/__tests__/useConnection.test.js
@@ -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',
});
diff --git a/commands/serve/web/hooks/__tests__/useOpenApp.test.js b/commands/serve/web/hooks/__tests__/useOpenApp.test.js
index 965a5955a..26bca959b 100644
--- a/commands/serve/web/hooks/__tests__/useOpenApp.test.js
+++ b/commands/serve/web/hooks/__tests__/useOpenApp.test.js
@@ -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(() => {
diff --git a/commands/serve/web/hooks/useConnection.js b/commands/serve/web/hooks/useConnection.js
index d79b9d880..f64afc6b1 100644
--- a/commands/serve/web/hooks/useConnection.js
+++ b/commands/serve/web/hooks/useConnection.js
@@ -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;
diff --git a/commands/serve/web/hooks/useOpenApp.js b/commands/serve/web/hooks/useOpenApp.js
index 652067207..fff9c62d8 100644
--- a/commands/serve/web/hooks/useOpenApp.js
+++ b/commands/serve/web/hooks/useOpenApp.js
@@ -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();
diff --git a/commands/serve/web/utils/__tests__/appLinkManager.test.js b/commands/serve/web/utils/__tests__/appLinkManager.test.js
index 77e9d923f..507f771c5 100644
--- a/commands/serve/web/utils/__tests__/appLinkManager.test.js
+++ b/commands/serve/web/utils/__tests__/appLinkManager.test.js
@@ -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}`);
diff --git a/commands/serve/web/utils/appLinkManager.js b/commands/serve/web/utils/appLinkManager.js
index fe289016e..eb5360622 100644
--- a/commands/serve/web/utils/appLinkManager.js
+++ b/commands/serve/web/utils/appLinkManager.js
@@ -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', ''));
diff --git a/commands/serve/web/utils/getCsrfToken.js b/commands/serve/web/utils/getCsrfToken.js
new file mode 100644
index 000000000..9288b259e
--- /dev/null
+++ b/commands/serve/web/utils/getCsrfToken.js
@@ -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 '';
+}