Skip to main content
Woohoo! Qlik Community has won “Best in Class Community” in the 2024 Khoros Kudos awards!
Announcements
Nov. 20th, Qlik Insider - Lakehouses: Driving the Future of Data & AI - PICK A SESSION
cancel
Showing results for 
Search instead for 
Did you mean: 
pixlics12
Contributor III
Contributor III

How to embed Qlik App/Sheet on the website

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):

  1. Tenant host
  2. Added my domain in the Content Security Policy section and selected frame-ancestors in the Directive
  3. Web Integration Id created in Web Integration Configuration section
  4. Client Id and secret created in OAuth section
  5. Public key, Private key created by following this article: Create Signed Tokens for JWT Authorization
  6. After following above article, I also have Issuer, Key ID which I setup in the Identity Provider section

Following 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:

console.log.png

 

Following is the network logs when I tried to run this code:

network.log.png

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

Labels (3)
24 Replies
alex_colombo
Employee
Employee

Hey @pixlics12 , about your errors, you are using JWT auth, so please check if JWT auth goes correctly and check if Qlik 3rd party cookie is set in browser developer tool. When enigma try to create the websocket, it should leverage the cookie set by JWT login. The call to endpoint /users/me return 401 because you are not authenticahed, and this is ok because the code is handling this part, indeed as next step it starts the JWT authentication. Please aslo check if your web app hostname has been added as origin in web integration id configuration.

About your questions; question number 1: at the moment you have to use Engine API (or enigma.js) for getting app sheets list. What you have in the example is correct.
Question number 2: the above code is using iframes the old way of doing this, I suggest to have a look to qlik-embed for embedding entire app/sheets. This is the newset way of doing what you are doing.
 

pixlics12
Contributor III
Contributor III
Author

Hello @alex_colombo, thank you for the reply.

I checked and verified that my web app hostname added as origin in web integration id configuration.

I also checked the qlik-embed but I am still struggling on how to pre authenticate the app/sheet, so that the visitor don't need to authenticate. I just want to integrate app/sheet programmatically so that it will be pre authenticate.

Can you please help me on this?

alex_colombo
Employee
Employee

Here you can find all the possible ways on qlik-embed and authentication. With this you can authenticate against Qlik tenant and then embed app/sheets.

pixlics12
Contributor III
Contributor III
Author

Hello @alex_colombo, Thank You for reaching out. I tried using the "Using OAuth clients" way.

It is working as:

First it will load the page like this with "Authorize" button:

pixlics12_0-1709114745957.png

Then after Authorized, it will start showing the targeted object:

pixlics12_2-1709114885861.png

But I don't want my users to authorize, I need to make it public, so that the objects/sheets/apps are pre-authorize using code.

I have an admin panel, where admin controls the main website. Admin authorize once in the admin panel, select the app > sheet OR app > sheet > object, to render on the main website. I want the same object/sheet should render on main website.

Admin once authorized so as I assume and did for other integrations, I stored the token of current user in safe place, and use same token to render sheet/object every time.

I also checked  but it will need an IdP (Identity Provider), but my account is not setup in that way. I am looking for direct solution.

Is there any way to did this? please help me.

Thanks You,

alex_colombo
Employee
Employee

Ok, for avoiding users to click on Qlik OAuth Authorize button you have to do two things.

First, please change consent method on OAuth configuration like below

Second, add data-auto-redirect attribute and set it to true like below

pixlics12
Contributor III
Contributor III
Author

Thank You @alex_colombo, I tried given changes, it is working, but only if a user is logged in on the same Qlik account on same browser window. If a user is not logged in then it is redirecting user to the Qlik login page. But it doesn't matter user is logged in or not. I need to show the sheet/object always.

Is this possible?

Thank You

alex_colombo
Employee
Employee

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.

pixlics12
Contributor III
Contributor III
Author

Thank you @alex_colombo, for the reply.

Yes I need the same, an anonymous user access.

I am just setting up the things as you mentioned, but can I setup these things on the free/trial account? I am able to see the 'Feature control' and 'Entitlements' in 'Settings', but some options are not visible like 'Creating Groups'.

So just curious to know, that free/trial account have ability for this or not,

pixlics12_0-1709204312031.png

Thank You.

alex_colombo
Employee
Employee

I think you can try with trial tenant, and as you mentioned, you could have less feature. If you don't have "Creating Groups", you cannot use groups for managing space access, instead use user Id for this testing purpose.