Unlock a world of possibilities! Login now and discover the exclusive benefits awaiting you.
I want to embed Qlik app/sheet
on my website. There are apps
on the Qlik, and the apps
contains the sheets
, which I want to embed for showing to website visitors/user. But users don't have access of Qlik, so I need to embed with some kind of authentication by code.
Before going on the code, I have following things created on the Qlik Management Console (QMC):
Tenant
hostContent Security Policy
section and selected frame-ancestors
in the Directive
Web Integration Id
created in Web Integration Configuration
sectionClient Id and secret
created in OAuth
sectionPublic key, Private
key created by following this article: Create Signed Tokens for JWT AuthorizationIssuer, Key ID
which I setup in the Identity Provider
sectionFollowing is my usecase
User first select an app
from the dropdown, this list populated through the following API:
GET https://QLIK-TENANT/api/v1/items?resourceType=app
Header: Bearer <token generated with OAuth Client id and secret>
And it is working.
After that I need to show another dropdown for all the sheets
of the selected app
. But I am unable to fetch the list of sheets. I checked there is no REST API which can give me sheets
by an app
.
Question 1: How can I get sheet list of the app? I also plan to list the sheets
using below JS code, but that is also not working.
Once user select app and sheet, then click on a button to Render sheet, and it should render that sheet on the webpage.
I tried using below piece of code (enigma js), referenced from here: Handle sheets in iframes with enigma.js
<html>
<head>
<script src="https://unpkg.com/enigma.js/enigma.min.js"></script>
</head>
<body>
<div id="main">
<div id="message"></div>
<iframe id="qlik_frame" style="border:none;width:100%;height:900px;"></iframe>
</div>
<script type="text/javascript">
// CONFIGURATION
const TENANT = "xxxxxxxxxxxxx.xx.qlikcloud.com";
const JWTENDPOINT = "https://example.com/api/v1/qlik/jwtToken"; // this endpoint create a JWT token using passed data and the private key mentioned in point #6 above
const WEBINTEGRATIONID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // mentioned in point #3 above
const APPID = "411111e0-36f8-4e6d-b25d-f0xxxxxxxxca"; // got using the REST API mentioned above
const IDENTITY = "xxxxxxxxxxxxxx"; // An arbitrary string to establish a separate session state.
// MAIN
(async function main() {
const isLoggedIn = await qlikLogin();
const qcsHeaders = await getQCSHeaders();
const [session, enigmaApp] = await connectEnigma(qcsHeaders, APPID, IDENTITY);
handleDisconnect(session);
const theme = await getTheme(enigmaApp);
const spaceId = (await getApp(APPID)).spaceId;
const spaceType = await getSpaceType(spaceId);
const sheets = await getSheetList(enigmaApp, spaceType);
renderSingleIframe("qlik_frame", APPID, sheets[0].qInfo.qId, theme, IDENTITY);
})();
// LOGIN
async function qlikLogin() {
const loggedIn = await checkLoggedIn();
if (loggedIn.status !== 200) {
const $jwtPostData = {
kid: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // mentioned in point #6 above
iss: "xxxxxxxxxxxxx.xx.qlikcloud.com",
name: "?????????" // what it should be????
};
const tokenRes = await (await getJWTToken(JWTENDPOINT, $jwtPostData));
const loginRes = await jwtLogin(tokenRes.body);
if (loginRes.status != 200) {
const message = "Something went wrong while logging in.";
throw new Error(message);
}
const recheckLoggedIn = await checkLoggedIn();
if (recheckLoggedIn.status !== 200) {
const message = "Third-party cookies are not enabled in your browser settings and/or browser mode.";
throw new Error(message);
}
}
return true;
}
async function checkLoggedIn() {
return await fetch(`https://${TENANT}/api/v1/users/me`, {
mode: "cors",
credentials: "include",
headers: {
"qlik-web-integration-id": WEBINTEGRATIONID
},
})
}
// Get Method the JWT and use it to obtain Qlik Cloud session cookie.
async function getJWTToken(jwtEndpoint, payloadData) {
try {
// Define the headers for the request, including the content type
const headers = new Headers({
"Content-Type": "application/json",
});
// Create a request object with the specified method, headers, and body
const requestOptions = {
method: "POST",
headers: headers,
mode: "cors",
body: JSON.stringify(payloadData), // Convert payloadData to JSON
};
// Use the fetch function to make a POST request to the specified endpoint
const response = await fetch(jwtEndpoint, requestOptions);
// Check if the response status code indicates success (e.g., 200 OK)
if (response.ok) {
// Parse the response body as JSON, assuming it contains the JWT
const jwtData = await response.json();
return jwtData; // Return the JWT data
} else {
// Handle HTTP error responses here if needed
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
// Handle any exceptions that may occur during the fetch request
console.error("Error fetching JWT:", error);
throw error;
}
}
async function jwtLogin(token) {
const authHeader = `Bearer ${token}`;
return await fetch(`https://${TENANT}/login/jwt-session?qlik-web-integration-id=${WEBINTEGRATIONID}`, {
credentials: "include",
mode: "cors",
method: "POST",
headers: {
"Authorization": authHeader,
"qlik-web-integration-id": WEBINTEGRATIONID
},
})
}
async function getQCSHeaders() {
const response = await fetch(`https://${TENANT}/api/v1/csrf-token`, {
mode: "cors",
credentials: "include",
headers: {
"qlik-web-integration-id": WEBINTEGRATIONID
},
})
const csrfToken = new Map(response.headers).get("qlik-csrf-token");
return {
"qlik-web-integration-id": WEBINTEGRATIONID,
"qlik-csrf-token": csrfToken,
};
}
// ENIGMA ENGINE CONNECTION
async function connectEnigma(qcsHeaders, appId, identity) {
const [session, app] = await getEnigmaSessionAndApp(appId, qcsHeaders, identity);
return [session, app];
}
async function getEnigmaSessionAndApp(appId, headers, identity) {
const params = Object.keys(headers)
.map((key) => `${key}=${headers[key]}`)
.join("&");
return (async () => {
const schema = await (await fetch("https://unpkg.com/enigma.js@2.7.0/schemas/12.612.0.json")).json();
try {
return await createEnigmaAppSession(schema, appId, identity, params);
} catch {
// If the socket is closed immediately following the connection this
// could be due to an edge-case race condition where the newly created
// user does not yet have access to the app due to access control propagation.
// This bit of code will make another attempt after a 1.5 seconds.
const waitSecond = await new Promise(resolve => setTimeout(resolve, 1500));
try {
return await createEnigmaAppSession(schema, appId, identity, params);
} catch (e) {
throw new Error(e);
}
}
})();
}
async function createEnigmaAppSession(schema, appId, identity, params) {
const session = enigma.create({
schema,
url: `wss://${TENANT}/app/${appId}/identity/${identity}?${params}`,
createSocket: url => new WebSocket(`wss://${TENANT}/app/${appId}/identity/${identity}?${params}`),
});
const enigmaGlobal = await session.open();
const enigmaApp = await enigmaGlobal.openDoc(appId);
return [session, enigmaApp];
}
// HANDLE ENGINE SESSION CLOSURE
function handleDisconnect(session) {
session.on("closed", () => {
console.log("Due to inactivity or loss of connection, this session has ended.");
});
session.on("suspended", () => {
console.log("Due to loss of connection, this session has been suspended.");
});
window.addEventListener("offline", () => {
session.close();
});
}
// GET QLIK APP (FOR SPACE ID)
async function getApp(appId) {
var url = new URL(`https://${TENANT}/api/v1/items?resourceType=app&resourceId=${appId}`);
const response = await fetch(url, {
method: "GET",
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/json",
"qlik-web-integration-id": WEBINTEGRATIONID,
},
})
responseJson = await response.json();
return responseJson.data[0];
}
// GET SPACE (FOR SPACE TYPE)
async function getSpaceType(spaceId) {
var url = new URL(`https://${TENANT}/api/v1/spaces/${spaceId}`);
const response = await fetch(url, {
method: "GET",
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/json",
"qlik-web-integration-id": WEBINTEGRATIONID,
},
})
responseJson = await response.json();
return responseJson.type;
}
// GET THEME
async function getTheme(enigmaApp) {
const createAppProps = await enigmaApp.createSessionObject({
qInfo: {
qId: "AppPropsList",
qType: "AppPropsList"
},
qAppObjectListDef: {
qType: "appprops",
qData: {
theme: "/theme"
}
}
});
const appProps = await enigmaApp.getObject("AppPropsList");
const appPropsLayout = await appProps.getLayout();
const theme = appPropsLayout.qAppObjectList.qItems[0].qData.theme;
return theme;
}
// GET SHEETS (WITH TYPE ADDED, E.G., BASE, COMMUNITY, PRIVATE)
async function getSheetList(app, spaceType) {
var sheets = await app.getObjects({
"qOptions": {
"qTypes": [
"sheet"
],
"qIncludeSessionObjects": false,
"qData": {}
}
})
sheetsIncludingType = [];
for await (const sheet of sheets) {
var pushSheet = true;
const sheetObject = await app.getObject(sheet.qInfo.qId);
const sheetLayout = await sheetObject.getLayout();
var isManaged = spaceType === "managed";
var sheetTypeEnum = 1;
var approved = sheet.qMeta.approved;
var published = sheet.qMeta.published;
const sheetTypeEnums = {
1: "Base",
2: "Community",
3: "Private"
};
if (!approved && !published) {
sheetTypeEnum = 3;
} else if (!approved && published) {
if (isManaged) {
sheetTypeEnum = 2;
}
}
const sheetTypeObject = {
"sheetType": sheetTypeEnums[sheetTypeEnum],
"sheetTypeEnum": sheetTypeEnum
};
const mergedObject = {
...sheetLayout,
...sheetTypeObject
};
sheetsIncludingType.push(mergedObject)
}
sheetsIncludingType.sort((a, b) => (a.sheetTypeEnum - b.sheetTypeEnum || a.rank - b.rank))
return sheetsIncludingType;
}
// HELPER FUNCTION TO GENERATE IFRAME
function renderSingleIframe(frameId, appId, sheetId, theme, identity) {
const frameUrl = `https://${TENANT}/single/?appid=${appId}&sheet=${sheetId}&theme=${theme}&identity=${identity}&opt=ctxmenu,currsel`;
document.getElementById(frameId).setAttribute("src", frameUrl);
}
</script>
</body>
</html>
But when I tried to run, it is not working and returned following JS
error in console, related to Socket
:
Following is the network logs when I tried to run this code:
Question 2: How to embed sheet/app
? In above code, Am I doing something wrong? Or if there are any other way to embed, please suggest me. Because I doubt, that it will need to create a lot of resources on the QMC
. So there should be some API
, to authenticate user, get token and use token to get embed code for the sheet
.
Please help me. Any suggestion will also be helpful.
Thank you
Thank You @alex_colombo for the reply.
I tried with the Qlik trial account/tenant but that account doesn't have the 'Identity Provider' menu.
So recently, I also arranged another Qlik account with premium subscription. Can I create APIs at my own server instead of AWS Lambda platform?
Thanks,
I'm not understanding the question, which API do you want to create? It is something related to Qlik?
I mean, the link you shared for "Embed content using IFrames and anonymous access", have some piece of code which setup on the AWS Lambda function, can I do same code at my own server?
yes you can use whatever tool/language you prefer for generating the JWT tokens.
Thank You @alex_colombo. I go through all the details you shared in this whole conversation and here Embed content using iframes and anonymous access.
-----------------------------------------------------------------------------------
Following is Your reply, sent Yesterday:
Basically, if I understood correctly, you need an anonymous user access, correct? To do so, at the moment, you have to use JWT as IdP, and passing your anonymous user. This will create new users into the tenant. Here you can find a link where it is explained. It is best practice to setting up anonymous access on a tenant should be done on a dedicated tenant.
On Management Console side, below few optional things to do:
Go to Management Console → Configuration → Settings → Feature control. Switch off all except for Creating Groups. If it is already switched off, turn on. This disable some features for anonymous users, it is up to you if this is needed or not.
Under Entitlements section ensure that you also have turned off Enable dynamic assignment of professional users and Enable dynamic assignment of analyzer users. This will not assigned license to anonymous users. As mentioned above, please use a dedicated tenant for anonymous access.
-----------------------------------------------------------------------------------
I find out and conclude that I already followed all the steps, and also have all the things which mentioned in this article (Embed content using iframes and anonymous access) as I also mentioned in my top question.
The code is also quite similar, just some minor differences are there. When I tried steps of this article, I reached at same error which I asked in the top question:
Console error
Network Log
-----------------------------------------------------------------------------------
One more thing I noticed today, that the new code is creating an anonymous user every time when I run my code.
-----------------------------------------------------------------------------------
As I replied that I have arranged another Qlik account with premium subscription, I did setup on same account, and above attached screenshots are for same premium account.
I don't have full access of that account, so below things are remaining to setup as you said:
- Management Console → Configuration → Settings → Feature control/Entitlements
Is this mandatory steps to follow in QMC, or just optional?
Thank You 🙂
Those steps are not mandatory, it depends which kind of features you want to provide to your anonymous users. About the error, I should see it in actions and troubleshoot it, I can't help much than this at this stage. I just want to highlight that where you see a 401 error on users/me API call, this is correct becuase at first time you are not logged in, that's why you received 401.
ok @alex_colombo, Thank you and appreciate your efforts and help.
I will reach out to you, if need something on this.
Thank You very much. 👍🏻
Hello @alex_colombo,
Thank you for all of your help, now finally I am able to embed sheet of an app, using the code which I asked in the question using Enigma.js.
One thing I tried, and it works. I made some changes in the code which generates JWT token. Now I am using following Payload:
$payload = array(
"jti" => guidv4(),
"sub" => $sub,
"subType" => "user",
"name" => 'Qlik Test',
"email" => 'qliktest@xyz.com',
"email_verified" => true,
"exp" => $expir,
"nbf" => $not_before,
"iat" => $current_time,
"aud" => "qlik.api/login/jwt-session",
"iss" => $tenant,
"groups" => array('anonymous')
);
Prior, I was using the random "name" and "email". But now I am using the "name" and "email" of my user. And I am using same email to login on the Qlik Cloud.And after this change I am able to embed sheet of an app.
----------------------------------------------------------------------------------
But I am facing an issue. I am able to list all of the apps by following API which I am seeing on the Qlik Cloud:
GET https://xxxxxxxxxxxxxxx.us.qlikcloud.com/api/v1/items?resourceType=app
But I am unable to embed the sheets of all the apps.
I tried to find out the root cause, and got that I can embed the sheets of only app where I am the owner.
There are more than 300 other apps listed on the Qlik Cloud, and I am unable to embed them. I can view all of the apps and sheets on the Qlik Cloud.
If I am able to view the app on the Qlik, then it should be embed too.
Can you please help to figure out what I am missing. I think there are some permission related issue. Or only the creator/owner of the app can embed the app.
EDIT: One more thing I noticed, all other apps have spaceId, but the app which I owned don't have spaceId,
Thank You
Also, can you please answer last question and mention all of the issues which we discussed in this conversation in a single message, so that I will mark and accept that as a solution. It will be helpful for other people also.
@pixlics12 try to open the app and check if you are able to see the sheets. Maybe sheets are not published by app owner, that's why you are not able to see them.
About marking the solution as accepted, please pick one and mark it, it is enugh.